moxml 0.5.47 → 0.5.49

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: 83f4ac58d2c22de225ec143ff3e78eb95e3460863b8fb1d67e8230a872bfad2a
4
- data.tar.gz: c7401f4154a32e534a1c6a53f067a25406b16056de651eec8e768ac04c602dcc
3
+ metadata.gz: 1e7f648665fbf11423b43ed957f111cabe57a732b2037d6e7d6d718beae4e0c3
4
+ data.tar.gz: ec4ac686e3c83b794944c02a3f6b92aa964c9e984fb5752b49dbeb08812e0f61
5
5
  SHA512:
6
- metadata.gz: d0ad7a04cbac82f8195be3be4318ee7d381974a812c7c2fce45110778875abfcb0be7ebb0c8e19339ddf2c2d8d9f4bd575254062b492a9cf4ad35072d8fe67fd
7
- data.tar.gz: 301188cc44df6af9339502db3694651cc2a69238e3c222f6c0cda92393e72b80d5353cca335671be6b6560d5d64da4c32ad3bc4484f91d12239ddebf677e6aec
6
+ metadata.gz: ad7b15e82e237a726680c1eed20c0942abbcc74aca205a58681c745548e5a5c9c5048034079b88b1c8f91eddeb742d0eebbd5cc45daca057f852d25da48eb331
7
+ data.tar.gz: 834577776b64caa59e9b363a8a936db9684a20ed7793a6698adbfe0d6633a0770e17f5d24b063352c3931bea1289ac53ceb0bd8c9f7b71ad3fe2f2007ef59a5f
@@ -133,6 +133,22 @@ module Moxml
133
133
  # Protocol-level native equality; engines with more than one
134
134
  # wrapper class over one C node override (leptris native
135
135
  # read layer). Shared adapter examples compare through this.
136
+ # Extend-in-place capability (#230): an adapter whose native
137
+ # objects can carry the contract modules directly returns the
138
+ # extended native here (its @native is itself); the default
139
+ # mints a wrapper shell.
140
+ def wrap_native(_node, _type, _context)
141
+ nil
142
+ end
143
+
144
+ # Subtree walk capability: an adapter with a C-side pre-order
145
+ # traversal (leptris >= 1.9.174.6 visit) walks descendants in
146
+ # one dispatch; nil keeps the recursive children walk. Self
147
+ # is not yielded (each_node semantics).
148
+ def walk_descendants(_native, _context)
149
+ nil
150
+ end
151
+
136
152
  def same_node?(one, other)
137
153
  one == other
138
154
  end
@@ -85,6 +85,24 @@ module Moxml
85
85
  defined?(::Leptris::XML::NativeNode) &&
86
86
  Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.5")
87
87
 
88
+ # Identity mode (#230 stage 2): the contract modules extended
89
+ # onto natives shadow the C surface (Element#[] over
90
+ # NativeNode#[]), so the adapter's native fast paths speak to
91
+ # the native layer through these bound entries — bind_call is
92
+ # the same C dispatch without the shadow lookup.
93
+ if NATIVE_READ_LAYER
94
+ NN = ::Leptris::XML::NativeNode
95
+ NN_ATTRIBUTE = NN.instance_method(:attribute)
96
+ NN_NAME = NN.instance_method(:name)
97
+ NN_PARENT = NN.instance_method(:parent)
98
+ NN_NEXT_SIBLING = NN.instance_method(:next_sibling)
99
+ NN_CONTENT = NN.instance_method(:content)
100
+ NN_CHILDREN = NN.instance_method(:children)
101
+ NN_NODE_TYPE = NN.instance_method(:node_type)
102
+ NN_DOCUMENT = NN.instance_method(:document)
103
+ NN_ADD_CHILD = NN.instance_method(:add_child)
104
+ end
105
+
88
106
  # 1.9.163.2 moved the native bulk children into C
89
107
  # (Native.bulk_children) and fixed the 512 truncation
90
108
  # (leptris-ruby#202). The 162.6-163.1 window carries the
@@ -136,6 +154,48 @@ module Moxml
136
154
  @native_doc_roots = ObjectSpace::WeakMap.new
137
155
 
138
156
  class << self
157
+ # Extend-in-place mint (#230 stage 2): a TypedData native
158
+ # carries the contract modules on itself — the wrapper layer's
159
+ # 3-4 Ruby frames collapse to the module call over the C
160
+ # method. @native is self; every adapter method that accepts
161
+ # a native already handles NativeNode receivers.
162
+ # Extend-in-place mint (#230 stage 2): disabled by
163
+ # measurement. Extending TypedData natives with the
164
+ # contract modules works correctly (full battery + lm
165
+ # 5422/0 under identity mode) but each extend allocates a
166
+ # singleton plus per-module iclasses — the consumer
167
+ # pipeline regressed 4017us -> 16350us with 6x the
168
+ # allocations on a 900-node walk. The per-read frame
169
+ # savings (~50ns) cannot repay that mint cost. Revisit
170
+ # when the binding grows klass-injection
171
+ # (NativeNode.from(doc, ptr, klass:) minting the
172
+ # contract-carrying subclass in C) — asked upstream in the
173
+ # #230 thread. Wrappers mint through the Wrappers:: shells
174
+ # meanwhile.
175
+ def wrap_native(_node, _type, _context)
176
+ nil
177
+ end
178
+
179
+ NATIVE_C_WALK =
180
+ defined?(::Leptris::XML::NativeNode) &&
181
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.174.6")
182
+
183
+ # One C visit walk (TODO.perf/27: post-order/abort state in
184
+ # the C callback, rb_yield direct — no FFI closure per
185
+ # node) instead of per-level children walks. Pre-order via
186
+ # entering; depth 0 is the receiver and stays unyielded.
187
+ def walk_descendants(native, context)
188
+ return nil unless NATIVE_C_WALK
189
+
190
+ root = to_binding(native)
191
+ return nil unless root.is_a?(::Leptris::XML::Element) ||
192
+ root.is_a?(::Leptris::XML::Document)
193
+
194
+ root.visit do |node, entering, depth|
195
+ yield Moxml::Node.wrap(node, context) if entering && depth.positive?
196
+ end
197
+ end
198
+
139
199
  def record_native_doc(root_native, doc)
140
200
  @native_doc_roots[root_native] = doc
141
201
  end
@@ -165,7 +225,7 @@ module Moxml
165
225
  def doc_for(node)
166
226
  # The 1.9.163.5 native layer exposes the owning document
167
227
  # directly; the root climb serves older bindings.
168
- return node.document if NATIVE_MUTATIONS_COHERENT
228
+ return NN_DOCUMENT.bind_call(node) if NATIVE_MUTATIONS_COHERENT
169
229
 
170
230
  current = node
171
231
  current = current.parent while current&.parent
@@ -201,7 +261,7 @@ module Moxml
201
261
  ::Leptris::XML::FFI::C14N_1_0, nil,
202
262
  mode: ::Leptris::XML::FFI::C14N_MODE_CANONICAL
203
263
  )
204
- wrapper = Moxml::Document.new(native_doc, Moxml::Context.new(:leptris))
264
+ wrapper = Moxml::Wrappers::Document.new(native_doc, Moxml::Context.new(:leptris))
205
265
  reference = Moxml::C14n::Inclusive10.new.canonicalize(wrapper.root)
206
266
  native == reference
207
267
  rescue StandardError
@@ -333,7 +393,14 @@ module Moxml
333
393
  # document-plus-attachment probe here cost more than the
334
394
  # read itself).
335
395
  def bare_attr_value(element, name)
336
- value = element[name.to_s]
396
+ # Exact-name C read: bare names target no-namespace
397
+ # attributes (the documented contract — a bare "y" never
398
+ # resolves a "p:y" sibling).
399
+ value = if NATIVE_READ_LAYER && element.is_a?(NN)
400
+ NN_ATTRIBUTE.bind_call(element, name.to_s)
401
+ else
402
+ element[name.to_s]
403
+ end
337
404
  if !NATIVE_STRINGS_UTF8 && NATIVE_READ_LAYER &&
338
405
  element.is_a?(::Leptris::XML::NativeNode) &&
339
406
  value.is_a?(String)
@@ -376,7 +443,7 @@ module Moxml
376
443
  create_document
377
444
  end
378
445
  ctx = _context || Context.new(:leptris)
379
- doc = Document.new(native_doc, ctx)
446
+ doc = Wrappers::Document.new(native_doc, ctx)
380
447
 
381
448
  if options[:noblanks] && !ENGINE_NOBLANKS_SAFE
382
449
  # The engine's DROP_WS_TEXT trims boundary whitespace of
@@ -451,7 +518,7 @@ module Moxml
451
518
  end
452
519
  attachments.set(native_doc, :entity_markers, false)
453
520
  bump_serialize_generation
454
- Document.new(native_doc, _context || Context.new(:leptris))
521
+ Wrappers::Document.new(native_doc, _context || Context.new(:leptris))
455
522
  end
456
523
 
457
524
  # nil when no parse flag is requested — the binding treats a
@@ -711,13 +778,15 @@ module Moxml
711
778
  end
712
779
 
713
780
  def processing_instruction_target(node)
781
+ return to_binding(node).target if NATIVE_READ_LAYER && node.is_a?(NN)
782
+
714
783
  node.target
715
784
  end
716
785
 
717
786
  def node_type(node)
718
787
  if NATIVE_READ_LAYER &&
719
788
  node.is_a?(::Leptris::XML::NativeNode)
720
- type = node.node_type
789
+ type = NN_NODE_TYPE.bind_call(node)
721
790
  # The native layer spells PIs :pi; the wrapper contract
722
791
  # (node_type_map) says :processing_instruction.
723
792
  return :processing_instruction if type == :pi
@@ -765,11 +834,12 @@ module Moxml
765
834
 
766
835
  if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
767
836
  # PIs expose no name through the native layer.
768
- return to_binding(node).target.to_s if node.node_type == :pi
837
+ return to_binding(node).target.to_s if NN_NODE_TYPE.bind_call(node) == :pi
769
838
 
770
- return node.name if NATIVE_STRINGS_UTF8
839
+ name = NN_NAME.bind_call(node)
840
+ return name if NATIVE_STRINGS_UTF8
771
841
 
772
- node.name.dup.force_encoding(Encoding::UTF_8)
842
+ name.dup.force_encoding(Encoding::UTF_8)
773
843
  end
774
844
 
775
845
  node.name.to_s.dup.force_encoding("UTF-8")
@@ -808,7 +878,7 @@ module Moxml
808
878
  # the wrapper's memo supplies the flag, so deriving it
809
879
  # here costs no C parent climb per call.
810
880
  natives = if NATIVE_BULK_FIXED
811
- node.children.to_a
881
+ NN_CHILDREN.bind_call(node).to_a
812
882
  else
813
883
  bulk_native_children(node)
814
884
  end
@@ -864,7 +934,7 @@ module Moxml
864
934
 
865
935
  def parent(node)
866
936
  if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
867
- parent = node.parent
937
+ parent = NN_PARENT.bind_call(node)
868
938
  return parent if parent
869
939
 
870
940
  doc = doc_for(node)
@@ -987,7 +1057,7 @@ module Moxml
987
1057
  end
988
1058
 
989
1059
  def next_sibling(node)
990
- return node.next_sibling if NATIVE_READ_LAYER &&
1060
+ return NN_NEXT_SIBLING.bind_call(node) if NATIVE_READ_LAYER &&
991
1061
  node.is_a?(::Leptris::XML::NativeNode)
992
1062
 
993
1063
  node.next_sibling if node.is_a?(::Leptris::XML::Node)
@@ -1114,6 +1184,12 @@ module Moxml
1114
1184
  bump_serialize_generation
1115
1185
  return child
1116
1186
  end
1187
+ if NATIVE_MUTATIONS_COHERENT && parent.is_a?(NN) && child.is_a?(NN)
1188
+ doc = NN_DOCUMENT.bind_call(parent)
1189
+ child = doc.create_text_node(child) if child.is_a?(String)
1190
+ return NN_ADD_CHILD.bind_call(parent, child)
1191
+ end
1192
+
1117
1193
  child = parent.document.create_text_node(child) if child.is_a?(String)
1118
1194
  parent.add_child(child)
1119
1195
  end
@@ -1200,9 +1276,10 @@ module Moxml
1200
1276
 
1201
1277
  def text_content(node)
1202
1278
  if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
1203
- return node.content if NATIVE_STRINGS_UTF8
1279
+ content = NN_CONTENT.bind_call(node)
1280
+ return content if NATIVE_STRINGS_UTF8
1204
1281
 
1205
- return node.content.dup.force_encoding(Encoding::UTF_8)
1282
+ content.dup.force_encoding(Encoding::UTF_8)
1206
1283
  end
1207
1284
 
1208
1285
  # Frequency-ordered: elements dominate reads; they paid
@@ -1246,26 +1323,35 @@ module Moxml
1246
1323
  end
1247
1324
 
1248
1325
  def cdata_content(node)
1326
+ return NN_CONTENT.bind_call(node) if NATIVE_READ_LAYER && node.is_a?(NN)
1327
+
1249
1328
  node.content
1250
1329
  end
1251
1330
 
1252
1331
  def set_cdata_content(node, content)
1332
+ node = to_binding(node) if NATIVE_READ_LAYER
1253
1333
  node.content = content.to_s
1254
1334
  end
1255
1335
 
1256
1336
  def comment_content(node)
1337
+ return NN_CONTENT.bind_call(node) if NATIVE_READ_LAYER && node.is_a?(NN)
1338
+
1257
1339
  node.content
1258
1340
  end
1259
1341
 
1260
1342
  def set_comment_content(node, content)
1343
+ node = to_binding(node) if NATIVE_READ_LAYER
1261
1344
  node.content = content.to_s
1262
1345
  end
1263
1346
 
1264
1347
  def processing_instruction_content(node)
1348
+ return NN_CONTENT.bind_call(node) if NATIVE_READ_LAYER && node.is_a?(NN)
1349
+
1265
1350
  node.content
1266
1351
  end
1267
1352
 
1268
1353
  def set_processing_instruction_content(node, content)
1354
+ node = to_binding(node) if NATIVE_READ_LAYER
1269
1355
  node.data = content.to_s
1270
1356
  end
1271
1357
 
@@ -152,7 +152,7 @@ module Moxml
152
152
  # any new parse-time logic only has to be added to this one
153
153
  # path; the restoration walk is just one of potentially several
154
154
  # post-processing steps and doesn't fork the construction.
155
- doc = Document.new(native_doc, ctx)
155
+ doc = Wrappers::Document.new(native_doc, ctx)
156
156
  Entity::Restorer.new(doc).run if ctx.config.restore_entities
157
157
  doc
158
158
  end
@@ -80,7 +80,7 @@ module Moxml
80
80
  # Use provided context if available, otherwise create new one
81
81
  ctx = _context || Context.new(:nokogiri)
82
82
  attachments.set(native_doc, :entity_markers, entity_markers)
83
- Document.new(native_doc, ctx)
83
+ Wrappers::Document.new(native_doc, ctx)
84
84
  end
85
85
 
86
86
  # Tolerant HTML parsing through libxml2's HTML mode: implied
@@ -90,7 +90,7 @@ module Moxml
90
90
  def parse_html(html, _options = {}, _context = nil)
91
91
  html_string = html.is_a?(IO) || html.is_a?(StringIO) ? html.read : html.to_s
92
92
  native_doc = ::Nokogiri::HTML(html_string)
93
- Document.new(native_doc, _context || Context.new(:nokogiri))
93
+ Wrappers::Document.new(native_doc, _context || Context.new(:nokogiri))
94
94
  end
95
95
 
96
96
  # Nokogiri parses with `config.recover` unless strict, so
@@ -54,7 +54,7 @@ module Moxml
54
54
  end
55
55
 
56
56
  ctx = _context || Context.new(:oga)
57
- doc = Document.new(native_doc, ctx)
57
+ doc = Wrappers::Document.new(native_doc, ctx)
58
58
  Entity::Restorer.new(doc).run if ctx.config.restore_entities
59
59
  doc
60
60
  end
@@ -68,7 +68,7 @@ module Moxml
68
68
  end
69
69
 
70
70
  ctx = _context || Context.new(context_adapter_name)
71
- Document.new(native_doc, ctx)
71
+ Wrappers::Document.new(native_doc, ctx)
72
72
  end
73
73
 
74
74
  # SAX parsing implementation for Ox
@@ -49,7 +49,7 @@ module Moxml
49
49
  end
50
50
 
51
51
  ctx = _context || Context.new(:rexml)
52
- doc = Document.new(native_doc, ctx)
52
+ doc = Wrappers::Document.new(native_doc, ctx)
53
53
  Entity::Restorer.new(doc).run if ctx.config.restore_entities
54
54
  doc
55
55
  end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- class Attribute < Node
4
+ module Attribute
5
+ include Node
6
+
5
7
  # Immutable between name= writes (which clear it via
6
8
  # clear_native_memo!), unlike element names that can change via
7
9
  # native adoption; the memo removes an adapter read per access.
@@ -64,7 +66,7 @@ module Moxml
64
66
 
65
67
  def namespace
66
68
  ns = adapter.namespace(@native)
67
- ns && Namespace.new(ns, context)
69
+ ns && Wrappers::Namespace.new(ns, context)
68
70
  end
69
71
 
70
72
  def namespace=(ns)
data/lib/moxml/cdata.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- class Cdata < Node
4
+ module Cdata
5
+ include Node
6
+
5
7
  def content
6
8
  adapter.cdata_content(@native)
7
9
  end
data/lib/moxml/comment.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- class Comment < Node
4
+ module Comment
5
+ include Node
6
+
5
7
  def content
6
8
  adapter.comment_content(@native)
7
9
  end
data/lib/moxml/context.rb CHANGED
@@ -77,7 +77,7 @@ module Moxml
77
77
  end
78
78
 
79
79
  def create_document(native_doc = nil)
80
- Document.new(config.adapter.create_document(native_doc), self)
80
+ Wrappers::Document.new(config.adapter.create_document(native_doc), self)
81
81
  end
82
82
 
83
83
  def parse(xml, options = {})
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- class Declaration < Node
4
+ module Declaration
5
+ include Node
6
+
5
7
  ALLOWED_VERSIONS = ["1.0", "1.1"].freeze
6
8
  ALLOWED_STANDALONE = %w[yes no].freeze
7
9
  ALLOWED_ATTRIBUTES = %w[version encoding standalone].freeze
data/lib/moxml/doctype.rb CHANGED
@@ -13,7 +13,9 @@ module Moxml
13
13
  # Most adapters do not currently implement these methods. If you need DOCTYPE
14
14
  # information, consider using adapter-specific methods or parsing the serialized
15
15
  # XML manually.
16
- class Doctype < Node
16
+ module Doctype
17
+ include Node
18
+
17
19
  # Returns the DOCTYPE name (root element name)
18
20
  #
19
21
  # @return [String, nil] the DOCTYPE name
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- class Document < Node
4
+ module Document
5
+ include Node
6
+
5
7
  attr_accessor :has_xml_declaration
6
8
 
7
9
  def initialize(native, context, adapter = nil, node_type = nil)
@@ -61,30 +63,30 @@ module Moxml
61
63
  end
62
64
 
63
65
  def create_element(name)
64
- Element.new(adapter.create_element(name, owner_doc: @native), context)
66
+ Wrappers::Element.new(adapter.create_element(name, owner_doc: @native), context)
65
67
  end
66
68
 
67
69
  def create_text(content)
68
- Text.new(adapter.create_text(content, owner_doc: @native), context)
70
+ Wrappers::Text.new(adapter.create_text(content, owner_doc: @native), context)
69
71
  end
70
72
 
71
73
  def create_cdata(content)
72
- Cdata.new(adapter.create_cdata(content, owner_doc: @native), context)
74
+ Wrappers::Cdata.new(adapter.create_cdata(content, owner_doc: @native), context)
73
75
  end
74
76
 
75
77
  def create_comment(content)
76
- Comment.new(adapter.create_comment(content, owner_doc: @native), context)
78
+ Wrappers::Comment.new(adapter.create_comment(content, owner_doc: @native), context)
77
79
  end
78
80
 
79
81
  def create_doctype(name, external_id, system_id)
80
- Doctype.new(
82
+ Wrappers::Doctype.new(
81
83
  adapter.create_doctype(name, external_id, system_id),
82
84
  context,
83
85
  )
84
86
  end
85
87
 
86
88
  def create_processing_instruction(target, content)
87
- ProcessingInstruction.new(
89
+ Wrappers::ProcessingInstruction.new(
88
90
  adapter.create_processing_instruction(target, content),
89
91
  context,
90
92
  )
@@ -93,11 +95,11 @@ module Moxml
93
95
  def create_declaration(version = "1.0", encoding = "UTF-8",
94
96
  standalone = nil)
95
97
  decl = adapter.create_declaration(version, encoding, standalone)
96
- Declaration.new(decl, context)
98
+ Wrappers::Declaration.new(decl, context)
97
99
  end
98
100
 
99
101
  def create_entity_reference(name)
100
- EntityReference.new(adapter.create_entity_reference(name), context)
102
+ Wrappers::EntityReference.new(adapter.create_entity_reference(name), context)
101
103
  end
102
104
 
103
105
  def add_child(node)
data/lib/moxml/element.rb CHANGED
@@ -120,7 +120,7 @@ module Moxml
120
120
  # mint, so the first name/value/attribute? access skips the
121
121
  # context hop and the adapter's type probe.
122
122
  @attributes ||= adapter.attributes(@native).map do |attr|
123
- a = Attribute.new(attr, context, adapter, :attribute)
123
+ a = Wrappers::Attribute.new(attr, context, adapter, :attribute)
124
124
  a.parent_node = self
125
125
  a
126
126
  end
@@ -182,7 +182,7 @@ module Moxml
182
182
  # it's NOT the same as namespaces.first
183
183
  def namespace
184
184
  ns = adapter.namespace(@native)
185
- ns && Namespace.new(ns, context)
185
+ ns && Wrappers::Namespace.new(ns, context)
186
186
  end
187
187
 
188
188
  # add the prefix to the element name
@@ -216,7 +216,7 @@ module Moxml
216
216
  return @namespace_definitions unless @namespace_definitions.nil?
217
217
 
218
218
  @namespace_definitions = adapter.namespace_definitions(@native).map do |ns|
219
- Namespace.new(ns, context)
219
+ Wrappers::Namespace.new(ns, context)
220
220
  end
221
221
  end
222
222
 
@@ -225,7 +225,7 @@ module Moxml
225
225
  # scope. The shape materialize records carry (issue #138).
226
226
  def declared_namespaces
227
227
  adapter.namespace_definitions(@native).map do |ns|
228
- wrapper = Namespace.new(ns, context)
228
+ wrapper = Wrappers::Namespace.new(ns, context)
229
229
  [wrapper.prefix, wrapper.uri]
230
230
  end
231
231
  end
@@ -236,7 +236,7 @@ module Moxml
236
236
  generation = context.namespace_scope_generation
237
237
  if @in_scope_namespaces.nil? || @in_scope_generation != generation
238
238
  @in_scope_namespaces = adapter.in_scope_namespaces(@native).map do |ns|
239
- Namespace.new(ns, context)
239
+ Wrappers::Namespace.new(ns, context)
240
240
  end
241
241
  @in_scope_generation = generation
242
242
  end
@@ -363,7 +363,8 @@ module Moxml
363
363
  end
364
364
  end
365
365
 
366
- class Element < Node
366
+ module Element
367
+ include Node
367
368
  include ElementBehavior
368
369
  end
369
370
  end
@@ -78,12 +78,12 @@ module Moxml
78
78
  def append_chunk(parent, type, payload)
79
79
  case type
80
80
  when :text
81
- parent.add_child(::Moxml::Text.new(
81
+ parent.add_child(::Moxml::Wrappers::Text.new(
82
82
  @adapter.create_native_text(payload), @ctx
83
83
  ))
84
84
  when :eref
85
85
  parent.add_child(
86
- ::Moxml::EntityReference.new(
86
+ ::Moxml::Wrappers::EntityReference.new(
87
87
  @adapter.create_native_entity_reference(payload),
88
88
  @ctx,
89
89
  ),
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- class EntityReference < Node
4
+ module EntityReference
5
+ include Node
6
+
5
7
  def content
6
8
  ""
7
9
  end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- class Namespace < Node
4
+ module Namespace
5
+ include Node
6
+
5
7
  def prefix
6
8
  adapter.namespace_prefix(@native)
7
9
  end
data/lib/moxml/node.rb CHANGED
@@ -20,15 +20,22 @@ module Moxml
20
20
  # otherwise pay the context hop and the type probe again on its
21
21
  # first access.
22
22
  def initialize(native, context, adapter = nil, node_type = nil)
23
+ prime_contract(native, context, adapter, node_type)
24
+ end
25
+
26
+ # Update native reference after identity-changing operations
27
+ # (e.g., LibXML doc.root= creates a new Ruby wrapper)
28
+ # Contract priming shared by the constructor and the extend-in-place
29
+ # mint (#230): an extended native IS its own @native.
30
+ def prime_contract(native, context, adapter = nil, node_type = nil)
23
31
  @context = context
24
32
  @native = native
25
33
  @parent_node = nil
26
34
  @adapter = adapter
27
35
  @node_type_cached = node_type
36
+ self
28
37
  end
29
38
 
30
- # Update native reference after identity-changing operations
31
- # (e.g., LibXML doc.root= creates a new Ruby wrapper)
32
39
  def refresh_native!(new_native)
33
40
  unless new_native.equal?(@native)
34
41
  context.unregister_wrapper(@native)
@@ -48,7 +55,7 @@ module Moxml
48
55
  end
49
56
 
50
57
  def document
51
- Document.wrap(adapter.document(@native), context)
58
+ Moxml::Node.wrap(adapter.document(@native), context)
52
59
  end
53
60
 
54
61
  def parent
@@ -255,7 +262,7 @@ module Moxml
255
262
  return nil unless element?
256
263
 
257
264
  ns = adapter.namespace(@native)
258
- ns && Namespace.new(ns, context)
265
+ ns && Wrappers::Namespace.new(ns, context)
259
266
  end
260
267
 
261
268
  # Returns all namespace definitions on this node
@@ -264,13 +271,26 @@ module Moxml
264
271
  return [] unless element?
265
272
 
266
273
  adapter.namespace_definitions(@native).map do |ns|
267
- Namespace.new(ns, context)
274
+ Wrappers::Namespace.new(ns, context)
268
275
  end
269
276
  end
270
277
 
271
278
  # Recursively yield all descendant nodes
272
279
  # Used by XPath descendant-or-self and descendant axes
273
280
  def each_node(&block)
281
+ unless block
282
+ # Eager materialization: the leptris C-side walk rb_yields
283
+ # from the C callback and segfaults across fiber-suspended
284
+ # enumerator frames, so the no-block form never enters it.
285
+ nodes = []
286
+ each_node { |node| nodes << node }
287
+ return nodes.each
288
+ end
289
+
290
+ # Adapters with a C-side subtree walk take it in one
291
+ # dispatch; the recursive children walk stays the fallback.
292
+ return if adapter.walk_descendants(@native, context, &block)
293
+
274
294
  children.each do |child|
275
295
  yield child
276
296
  child.each_node(&block)
@@ -414,7 +434,7 @@ module Moxml
414
434
  # Native equality goes through the adapter: engines with a
415
435
  # native read layer hand out two wrapper classes over one C
416
436
  # node, and raw native == is false across that seam.
417
- self.class == other.class &&
437
+ other.is_a?(Moxml::Node) &&
418
438
  adapter.same_node?(@native, other.native)
419
439
  end
420
440
 
@@ -486,7 +506,7 @@ module Moxml
486
506
 
487
507
  def prepare_node(node)
488
508
  case node
489
- when String then Text.new(adapter.create_text(node), context)
509
+ when String then Moxml::Node.wrap(adapter.create_text(node), context)
490
510
  when Node then node
491
511
  else
492
512
  raise Moxml::DocumentStructureError.new(
@@ -512,11 +532,35 @@ module Moxml
512
532
  end
513
533
  end
514
534
 
515
- class Node
535
+ module Node
516
536
  include NodeBehavior
517
537
 
538
+ # Instantiation shells per type — the contract constants
539
+ # (Element, Text, ...) are modules since issue #230, so wrappers
540
+ # mint through Wrappers::*.
518
541
  def self.node_type_map
519
542
  @node_type_map ||= {
543
+ element: Wrappers::Element,
544
+ text: Wrappers::Text,
545
+ cdata: Wrappers::Cdata,
546
+ comment: Wrappers::Comment,
547
+ processing_instruction: Wrappers::ProcessingInstruction,
548
+ document: Wrappers::Document,
549
+ declaration: Wrappers::Declaration,
550
+ doctype: Wrappers::Doctype,
551
+ attribute: Wrappers::Attribute,
552
+ entity_reference: Wrappers::EntityReference,
553
+ }.freeze
554
+ end
555
+
556
+ # The contract module for a type — what an in-place extended
557
+ # native carries instead of a wrapper shell.
558
+ def self.contract_module(type)
559
+ contract_modules[type]
560
+ end
561
+
562
+ def self.contract_modules
563
+ @contract_modules ||= {
520
564
  element: Element,
521
565
  text: Text,
522
566
  cdata: Cdata,
@@ -538,8 +582,17 @@ module Moxml
538
582
 
539
583
  adapter = adapter(context)
540
584
  type = adapter.node_type(node)
541
- klass = node_type_map[type] || self
542
585
 
586
+ # Extend-in-place (issue #230): adapters whose natives can
587
+ # carry the contract modules directly (leptris TypedData) mint
588
+ # the native itself as the wrapper — @native is self.
589
+ if (extended = adapter.wrap_native(node, type, context))
590
+ extended.prime_contract(node, context, adapter, type)
591
+ context.register_wrapper(node, extended)
592
+ return extended
593
+ end
594
+
595
+ klass = node_type_map[type] || Wrappers::Node
543
596
  klass.new(node, context, adapter, type)
544
597
  .tap { |wrapper| context.register_wrapper(node, wrapper) }
545
598
  end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- class ProcessingInstruction < Node
4
+ module ProcessingInstruction
5
+ include Node
6
+
5
7
  def target
6
8
  adapter.processing_instruction_target(@native)
7
9
  end
data/lib/moxml/text.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- class Text < Node
4
+ module Text
5
+ include Node
6
+
5
7
  def content
6
8
  text = raw_content
7
9
  entity_bearing? ? adapter.restore_entities(text) : text
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.47"
4
+ VERSION = "0.5.49"
5
5
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ # Instantiation shells (issue #230): the node-type constants are
5
+ # contract MODULES so leptris TypedData natives can carry them in
6
+ # place — wrappers are minted through these shells for native
7
+ # objects that cannot take the modules in place (binding-layer
8
+ # natives on every adapter, and all natives on non-leptris ones).
9
+ module Wrappers
10
+ Node = Class.new do
11
+ include Moxml::Node
12
+ end
13
+ Element = Class.new(Node) do
14
+ include Moxml::Element
15
+ end
16
+ Text = Class.new(Node) do
17
+ include Moxml::Text
18
+ end
19
+ Cdata = Class.new(Node) do
20
+ include Moxml::Cdata
21
+ end
22
+ Comment = Class.new(Node) do
23
+ include Moxml::Comment
24
+ end
25
+ ProcessingInstruction = Class.new(Node) do
26
+ include Moxml::ProcessingInstruction
27
+ end
28
+ Document = Class.new(Node) do
29
+ include Moxml::Document
30
+ end
31
+ Declaration = Class.new(Node) do
32
+ include Moxml::Declaration
33
+ end
34
+ Doctype = Class.new(Node) do
35
+ include Moxml::Doctype
36
+ end
37
+ Attribute = Class.new(Node) do
38
+ include Moxml::Attribute
39
+ end
40
+ EntityReference = Class.new(Node) do
41
+ include Moxml::EntityReference
42
+ end
43
+ Namespace = Class.new(Node) do
44
+ include Moxml::Namespace
45
+ end
46
+ end
47
+ end
data/lib/moxml.rb CHANGED
@@ -67,6 +67,7 @@ module Moxml
67
67
  autoload :Config, "moxml/config"
68
68
  autoload :Context, "moxml/context"
69
69
  autoload :Node, "moxml/node"
70
+ autoload :Wrappers, "moxml/wrappers"
70
71
  autoload :NodeSet, "moxml/node_set"
71
72
  autoload :LazyNodeSet, "moxml/lazy_node_set"
72
73
  autoload :Document, "moxml/document"
@@ -22,9 +22,8 @@
22
22
  describe "fragment parsing (issue #188)" do
23
23
  it "returns the fragment's top-level nodes uniformly" do
24
24
  nodes = ctx.parse_fragment(%(<a x="1">t1</a><b/>tail))
25
- expect(nodes.map(&:class)).to eq(
26
- [Moxml::Element, Moxml::Element, Moxml::Text],
27
- )
25
+ expect(nodes.map(&:element?)).to eq([true, true, false])
26
+ expect(nodes.map(&:text?)).to eq([false, false, true])
28
27
  expect(nodes[0]["x"]).to eq("1")
29
28
  expect(nodes[0].text).to eq("t1")
30
29
  expect(nodes[2].content).to eq("tail")
@@ -76,12 +76,12 @@ RSpec.shared_examples "Entity Reference Whitespace Preservation" do
76
76
  end
77
77
 
78
78
  children = doc.root.children
79
- types = children.map(&:class)
79
+ types = children.map(&:entity_reference?)
80
80
 
81
81
  expect(types).to eq([
82
- Moxml::EntityReference,
83
- Moxml::Text,
84
- Moxml::EntityReference,
82
+ true,
83
+ false,
84
+ true,
85
85
  ])
86
86
  expect(children[1].content).to eq(" ")
87
87
  end
@@ -93,10 +93,14 @@ RSpec.describe Moxml::Adapter::Leptris do
93
93
  doc = ctx.parse('<?xml version="1.0"?><?pi-prolog before?><root/><?pi-epilog after?><!-- tail -->')
94
94
  kids = doc.children.to_a
95
95
 
96
- expect(kids.map(&:class)).to eq(
97
- [Moxml::ProcessingInstruction, Moxml::Element,
98
- Moxml::ProcessingInstruction, Moxml::Comment],
99
- )
96
+ shape = kids.map do |k|
97
+ if k.processing_instruction?
98
+ :pi
99
+ else
100
+ k.element? ? :element : :comment
101
+ end
102
+ end
103
+ expect(shape).to eq(%i[pi element pi comment])
100
104
  expect(kids.select(&:processing_instruction?).map(&:target)).to eq(%w[pi-prolog pi-epilog])
101
105
  expect(kids[0].content).to eq("before")
102
106
  expect(doc.to_xml.index("pi-epilog")).to be > doc.to_xml.index("</root>")
@@ -606,14 +610,15 @@ RSpec.describe Moxml::Adapter::Leptris do
606
610
  it "skips the marker split for entity-free documents" do
607
611
  doc = ctx.parse("<root>\n <a>text</a>\n <b/>\n</root>")
608
612
  kids = doc.root.children.to_a
609
- expect(kids.map(&:class)).to eq([Moxml::Text, Moxml::Element, Moxml::Text, Moxml::Element, Moxml::Text])
613
+ shape = kids.map { |k| k.text? ? :text : :element }
614
+ expect(shape).to eq(%i[text element text element text])
610
615
  expect(described_class.entity_bearing?(doc.root.native)).to be(false)
611
616
  end
612
617
 
613
618
  it "splits markers when the source carries entities" do
614
619
  doc = ctx.parse("<root><a>pre&nbsp;post</a></root>")
615
620
  kids = doc.at_xpath("//a").children.to_a
616
- expect(kids.map(&:class)).to include(Moxml::EntityReference)
621
+ expect(kids.map { |k| k.is_a?(Moxml::EntityReference) }).to include(true)
617
622
  expect(described_class.entity_bearing?(doc.root.native)).to be(true)
618
623
  end
619
624
 
@@ -631,7 +636,7 @@ RSpec.describe Moxml::Adapter::Leptris do
631
636
  er = doc.create_entity_reference("nbsp")
632
637
  doc.at_xpath("//a").add_child(er)
633
638
  expect(described_class.entity_bearing?(doc.root.native)).to be(true)
634
- expect(doc.at_xpath("//a").children.to_a.map(&:class)).to include(Moxml::EntityReference)
639
+ expect(doc.at_xpath("//a").children.to_a.map { |k| k.is_a?(Moxml::EntityReference) }).to include(true)
635
640
  end
636
641
  end
637
642
 
@@ -702,11 +707,11 @@ RSpec.describe Moxml::Adapter::Leptris do
702
707
  expect(decl.digest).to be_nil
703
708
  end
704
709
  # comments and PIs hash in C
705
- kinds = doc.root.children.map { |n| [n.class, n.digest] }
706
- comment = kinds.find { |n, _| n == Moxml::Comment }
707
- pi = kinds.find { |n, _| n == Moxml::ProcessingInstruction }
708
- expect(comment[1]).to be_a(Integer)
709
- expect(pi[1]).to be_a(Integer)
710
+ kinds = doc.root.children.map { |n| [n.is_a?(Moxml::Comment), n.is_a?(Moxml::ProcessingInstruction), n.digest] }
711
+ comment = kinds.find { |c, _, _| c }
712
+ pi = kinds.find { |_, p, _| p }
713
+ expect(comment[2]).to be_a(Integer)
714
+ expect(pi[2]).to be_a(Integer)
710
715
  end
711
716
  end
712
717
  end
@@ -113,7 +113,7 @@ RSpec.describe Moxml::Adapter::Libxml do
113
113
  doc = context.parse("<root><a/><b/></root>")
114
114
  a = doc.root.children.first
115
115
  b = doc.root.children.last
116
- eref = Moxml::EntityReference.new(
116
+ eref = Moxml::Wrappers::EntityReference.new(
117
117
  adapter.create_native_entity_reference("amp"), context
118
118
  )
119
119
  a.add_child(eref)
@@ -125,7 +125,7 @@ RSpec.describe Moxml::Adapter::Libxml do
125
125
  it "returns [refs, sequence] when both are registered for the element" do
126
126
  doc = context.parse("<root><a>text</a></root>")
127
127
  a = doc.root.children.first
128
- eref = Moxml::EntityReference.new(
128
+ eref = Moxml::Wrappers::EntityReference.new(
129
129
  adapter.create_native_entity_reference("amp"), context
130
130
  )
131
131
  a.add_child(eref)
@@ -144,7 +144,7 @@ RSpec.describe Moxml::Adapter::Libxml do
144
144
  it "preserves normal child indentation when entity refs are present" do
145
145
  doc = context.parse("<root><a><b/></a></root>")
146
146
  a = doc.root.children.first
147
- eref = Moxml::EntityReference.new(
147
+ eref = Moxml::Wrappers::EntityReference.new(
148
148
  adapter.create_native_entity_reference("amp"), context
149
149
  )
150
150
  a.add_child(eref)
@@ -45,7 +45,7 @@ RSpec.describe Moxml::NodeSet do
45
45
  end
46
46
 
47
47
  it "wraps through the Enumerable surface" do
48
- expect(doc.root.children.map(&:class).uniq).to eq([Moxml::Element])
48
+ expect(doc.root.children.map { |c| c.is_a?(Moxml::Element) }.uniq).to eq([true])
49
49
  end
50
50
  end
51
51
  end
@@ -84,7 +84,7 @@ RSpec.describe Moxml::Node do
84
84
 
85
85
  describe "#content" do
86
86
  it "returns empty string on base Node" do
87
- expect(described_class.new(nil, context).content).to eq("")
87
+ expect(Moxml::Wrappers::Node.new(nil, context).content).to eq("")
88
88
  end
89
89
 
90
90
  it "returns text on Element" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.47
4
+ version: 0.5.49
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-15 00:00:00.000000000 Z
11
+ date: 2026-09-16 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Moxml is a unified XML manipulation library that provides a common API
@@ -271,6 +271,7 @@ files:
271
271
  - lib/moxml/signature/verifier.rb
272
272
  - lib/moxml/text.rb
273
273
  - lib/moxml/version.rb
274
+ - lib/moxml/wrappers.rb
274
275
  - lib/moxml/xml_emitter.rb
275
276
  - lib/moxml/xml_utils.rb
276
277
  - lib/moxml/xml_utils/encoder.rb