moxml 0.5.46 → 0.5.48

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: 3b15f3ffeac134fbaa674a8e2970ae5606dd1e08985f288d01c48323ffeca4f1
4
- data.tar.gz: 43f616e34f485d21bbc431cd42da7347e404647a71e0e84af697e205e24067a1
3
+ metadata.gz: fbe45e0b5017c94650ed10dde4e726843ad5a820005ac4a1f653d5747ef04f71
4
+ data.tar.gz: 14f003f4f15ff1b9a40429654a7435485cb20ca62e88f6b9dc21e1120e9c85dd
5
5
  SHA512:
6
- metadata.gz: 7007190eea32272f8be0de72b5a11f9b4d66cc0108cff530986443012a46814a886b34f925649593139c3d0cd7d4bf13d91d4088ae19be71ad150cc8464436de
7
- data.tar.gz: f967159613aa7c650d3d58c05b1dc7cb5376d975c01496e69aabc6f3c8d5631fcd9bb8dcb3c05320b3e345cf44fdd5cce922da0ca38079c4d7c322e39fb9dd72
6
+ metadata.gz: b6bcf2d9d9b6a34bbd56afc5076421c22e107ac33e9a8adc06ebe0329b7cab59e038ace45e0cb2d14debe8ba16c8cd606af49d8994bf59137e51d2679487295d
7
+ data.tar.gz: 22ea33b5bc99974709cc8cbcf670dc39fdce033fcce310353cda9aa15568129a283055a8e8975f211d67a8167fa6f87597db0d81e4935f77864a6d58ccc1a9dd
data/Gemfile CHANGED
@@ -9,7 +9,7 @@ gemspec
9
9
  gem "benchmark"
10
10
  gem "benchmark-ips"
11
11
  gem "get_process_mem"
12
- gem "leptris", "~> 1.9.174"
12
+ gem "leptris", "~> 1.9.174.1"
13
13
  gem "libxml-ruby", "~> 5.0"
14
14
  gem "nokogiri", "~> 1.18"
15
15
  gem "openssl", "~> 3.0"
@@ -133,6 +133,14 @@ 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
+
136
144
  def same_node?(one, other)
137
145
  one == other
138
146
  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,28 @@ 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
+
139
179
  def record_native_doc(root_native, doc)
140
180
  @native_doc_roots[root_native] = doc
141
181
  end
@@ -165,7 +205,7 @@ module Moxml
165
205
  def doc_for(node)
166
206
  # The 1.9.163.5 native layer exposes the owning document
167
207
  # directly; the root climb serves older bindings.
168
- return node.document if NATIVE_MUTATIONS_COHERENT
208
+ return NN_DOCUMENT.bind_call(node) if NATIVE_MUTATIONS_COHERENT
169
209
 
170
210
  current = node
171
211
  current = current.parent while current&.parent
@@ -201,7 +241,7 @@ module Moxml
201
241
  ::Leptris::XML::FFI::C14N_1_0, nil,
202
242
  mode: ::Leptris::XML::FFI::C14N_MODE_CANONICAL
203
243
  )
204
- wrapper = Moxml::Document.new(native_doc, Moxml::Context.new(:leptris))
244
+ wrapper = Moxml::Wrappers::Document.new(native_doc, Moxml::Context.new(:leptris))
205
245
  reference = Moxml::C14n::Inclusive10.new.canonicalize(wrapper.root)
206
246
  native == reference
207
247
  rescue StandardError
@@ -333,7 +373,14 @@ module Moxml
333
373
  # document-plus-attachment probe here cost more than the
334
374
  # read itself).
335
375
  def bare_attr_value(element, name)
336
- value = element[name.to_s]
376
+ # Exact-name C read: bare names target no-namespace
377
+ # attributes (the documented contract — a bare "y" never
378
+ # resolves a "p:y" sibling).
379
+ value = if NATIVE_READ_LAYER && element.is_a?(NN)
380
+ NN_ATTRIBUTE.bind_call(element, name.to_s)
381
+ else
382
+ element[name.to_s]
383
+ end
337
384
  if !NATIVE_STRINGS_UTF8 && NATIVE_READ_LAYER &&
338
385
  element.is_a?(::Leptris::XML::NativeNode) &&
339
386
  value.is_a?(String)
@@ -376,7 +423,7 @@ module Moxml
376
423
  create_document
377
424
  end
378
425
  ctx = _context || Context.new(:leptris)
379
- doc = Document.new(native_doc, ctx)
426
+ doc = Wrappers::Document.new(native_doc, ctx)
380
427
 
381
428
  if options[:noblanks] && !ENGINE_NOBLANKS_SAFE
382
429
  # The engine's DROP_WS_TEXT trims boundary whitespace of
@@ -451,7 +498,7 @@ module Moxml
451
498
  end
452
499
  attachments.set(native_doc, :entity_markers, false)
453
500
  bump_serialize_generation
454
- Document.new(native_doc, _context || Context.new(:leptris))
501
+ Wrappers::Document.new(native_doc, _context || Context.new(:leptris))
455
502
  end
456
503
 
457
504
  # nil when no parse flag is requested — the binding treats a
@@ -711,13 +758,15 @@ module Moxml
711
758
  end
712
759
 
713
760
  def processing_instruction_target(node)
761
+ return to_binding(node).target if NATIVE_READ_LAYER && node.is_a?(NN)
762
+
714
763
  node.target
715
764
  end
716
765
 
717
766
  def node_type(node)
718
767
  if NATIVE_READ_LAYER &&
719
768
  node.is_a?(::Leptris::XML::NativeNode)
720
- type = node.node_type
769
+ type = NN_NODE_TYPE.bind_call(node)
721
770
  # The native layer spells PIs :pi; the wrapper contract
722
771
  # (node_type_map) says :processing_instruction.
723
772
  return :processing_instruction if type == :pi
@@ -765,11 +814,12 @@ module Moxml
765
814
 
766
815
  if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
767
816
  # PIs expose no name through the native layer.
768
- return to_binding(node).target.to_s if node.node_type == :pi
817
+ return to_binding(node).target.to_s if NN_NODE_TYPE.bind_call(node) == :pi
769
818
 
770
- return node.name if NATIVE_STRINGS_UTF8
819
+ name = NN_NAME.bind_call(node)
820
+ return name if NATIVE_STRINGS_UTF8
771
821
 
772
- node.name.dup.force_encoding(Encoding::UTF_8)
822
+ name.dup.force_encoding(Encoding::UTF_8)
773
823
  end
774
824
 
775
825
  node.name.to_s.dup.force_encoding("UTF-8")
@@ -808,7 +858,7 @@ module Moxml
808
858
  # the wrapper's memo supplies the flag, so deriving it
809
859
  # here costs no C parent climb per call.
810
860
  natives = if NATIVE_BULK_FIXED
811
- node.children.to_a
861
+ NN_CHILDREN.bind_call(node).to_a
812
862
  else
813
863
  bulk_native_children(node)
814
864
  end
@@ -864,7 +914,7 @@ module Moxml
864
914
 
865
915
  def parent(node)
866
916
  if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
867
- parent = node.parent
917
+ parent = NN_PARENT.bind_call(node)
868
918
  return parent if parent
869
919
 
870
920
  doc = doc_for(node)
@@ -987,7 +1037,7 @@ module Moxml
987
1037
  end
988
1038
 
989
1039
  def next_sibling(node)
990
- return node.next_sibling if NATIVE_READ_LAYER &&
1040
+ return NN_NEXT_SIBLING.bind_call(node) if NATIVE_READ_LAYER &&
991
1041
  node.is_a?(::Leptris::XML::NativeNode)
992
1042
 
993
1043
  node.next_sibling if node.is_a?(::Leptris::XML::Node)
@@ -1114,6 +1164,12 @@ module Moxml
1114
1164
  bump_serialize_generation
1115
1165
  return child
1116
1166
  end
1167
+ if NATIVE_MUTATIONS_COHERENT && parent.is_a?(NN) && child.is_a?(NN)
1168
+ doc = NN_DOCUMENT.bind_call(parent)
1169
+ child = doc.create_text_node(child) if child.is_a?(String)
1170
+ return NN_ADD_CHILD.bind_call(parent, child)
1171
+ end
1172
+
1117
1173
  child = parent.document.create_text_node(child) if child.is_a?(String)
1118
1174
  parent.add_child(child)
1119
1175
  end
@@ -1200,9 +1256,10 @@ module Moxml
1200
1256
 
1201
1257
  def text_content(node)
1202
1258
  if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
1203
- return node.content if NATIVE_STRINGS_UTF8
1259
+ content = NN_CONTENT.bind_call(node)
1260
+ return content if NATIVE_STRINGS_UTF8
1204
1261
 
1205
- return node.content.dup.force_encoding(Encoding::UTF_8)
1262
+ content.dup.force_encoding(Encoding::UTF_8)
1206
1263
  end
1207
1264
 
1208
1265
  # Frequency-ordered: elements dominate reads; they paid
@@ -1246,26 +1303,35 @@ module Moxml
1246
1303
  end
1247
1304
 
1248
1305
  def cdata_content(node)
1306
+ return NN_CONTENT.bind_call(node) if NATIVE_READ_LAYER && node.is_a?(NN)
1307
+
1249
1308
  node.content
1250
1309
  end
1251
1310
 
1252
1311
  def set_cdata_content(node, content)
1312
+ node = to_binding(node) if NATIVE_READ_LAYER
1253
1313
  node.content = content.to_s
1254
1314
  end
1255
1315
 
1256
1316
  def comment_content(node)
1317
+ return NN_CONTENT.bind_call(node) if NATIVE_READ_LAYER && node.is_a?(NN)
1318
+
1257
1319
  node.content
1258
1320
  end
1259
1321
 
1260
1322
  def set_comment_content(node, content)
1323
+ node = to_binding(node) if NATIVE_READ_LAYER
1261
1324
  node.content = content.to_s
1262
1325
  end
1263
1326
 
1264
1327
  def processing_instruction_content(node)
1328
+ return NN_CONTENT.bind_call(node) if NATIVE_READ_LAYER && node.is_a?(NN)
1329
+
1265
1330
  node.content
1266
1331
  end
1267
1332
 
1268
1333
  def set_processing_instruction_content(node, content)
1334
+ node = to_binding(node) if NATIVE_READ_LAYER
1269
1335
  node.data = content.to_s
1270
1336
  end
1271
1337
 
@@ -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
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- class Element < Node
4
+ # Instance behavior for Moxml::Element, extracted so the leptris
5
+ # adapter can extend natives with it in place (issue #230); the
6
+ # class remains the consumer-facing contract.
7
+ module ElementBehavior
5
8
  def name
6
9
  # The adapter read is an FFI call plus (on several adapters) a
7
10
  # defensive string copy; names only change through name= and
@@ -117,7 +120,7 @@ module Moxml
117
120
  # mint, so the first name/value/attribute? access skips the
118
121
  # context hop and the adapter's type probe.
119
122
  @attributes ||= adapter.attributes(@native).map do |attr|
120
- a = Attribute.new(attr, context, adapter, :attribute)
123
+ a = Wrappers::Attribute.new(attr, context, adapter, :attribute)
121
124
  a.parent_node = self
122
125
  a
123
126
  end
@@ -179,7 +182,7 @@ module Moxml
179
182
  # it's NOT the same as namespaces.first
180
183
  def namespace
181
184
  ns = adapter.namespace(@native)
182
- ns && Namespace.new(ns, context)
185
+ ns && Wrappers::Namespace.new(ns, context)
183
186
  end
184
187
 
185
188
  # add the prefix to the element name
@@ -213,7 +216,7 @@ module Moxml
213
216
  return @namespace_definitions unless @namespace_definitions.nil?
214
217
 
215
218
  @namespace_definitions = adapter.namespace_definitions(@native).map do |ns|
216
- Namespace.new(ns, context)
219
+ Wrappers::Namespace.new(ns, context)
217
220
  end
218
221
  end
219
222
 
@@ -222,7 +225,7 @@ module Moxml
222
225
  # scope. The shape materialize records carry (issue #138).
223
226
  def declared_namespaces
224
227
  adapter.namespace_definitions(@native).map do |ns|
225
- wrapper = Namespace.new(ns, context)
228
+ wrapper = Wrappers::Namespace.new(ns, context)
226
229
  [wrapper.prefix, wrapper.uri]
227
230
  end
228
231
  end
@@ -233,7 +236,7 @@ module Moxml
233
236
  generation = context.namespace_scope_generation
234
237
  if @in_scope_namespaces.nil? || @in_scope_generation != generation
235
238
  @in_scope_namespaces = adapter.in_scope_namespaces(@native).map do |ns|
236
- Namespace.new(ns, context)
239
+ Wrappers::Namespace.new(ns, context)
237
240
  end
238
241
  @in_scope_generation = generation
239
242
  end
@@ -359,4 +362,9 @@ module Moxml
359
362
  context.bump_namespace_scope_generation
360
363
  end
361
364
  end
365
+
366
+ module Element
367
+ include Node
368
+ include ElementBehavior
369
+ end
362
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
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- class Node
4
+ # Instance behavior for Moxml::Node, extracted so the leptris
5
+ # adapter can extend natives with it in place (issue #230); the
6
+ # class remains the consumer-facing contract and the wrap factory.
7
+ module NodeBehavior
5
8
  include XmlUtils
6
9
  include Enumerable
7
10
 
@@ -17,15 +20,22 @@ module Moxml
17
20
  # otherwise pay the context hop and the type probe again on its
18
21
  # first access.
19
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)
20
31
  @context = context
21
32
  @native = native
22
33
  @parent_node = nil
23
34
  @adapter = adapter
24
35
  @node_type_cached = node_type
36
+ self
25
37
  end
26
38
 
27
- # Update native reference after identity-changing operations
28
- # (e.g., LibXML doc.root= creates a new Ruby wrapper)
29
39
  def refresh_native!(new_native)
30
40
  unless new_native.equal?(@native)
31
41
  context.unregister_wrapper(@native)
@@ -45,7 +55,7 @@ module Moxml
45
55
  end
46
56
 
47
57
  def document
48
- Document.wrap(adapter.document(@native), context)
58
+ Moxml::Node.wrap(adapter.document(@native), context)
49
59
  end
50
60
 
51
61
  def parent
@@ -252,7 +262,7 @@ module Moxml
252
262
  return nil unless element?
253
263
 
254
264
  ns = adapter.namespace(@native)
255
- ns && Namespace.new(ns, context)
265
+ ns && Wrappers::Namespace.new(ns, context)
256
266
  end
257
267
 
258
268
  # Returns all namespace definitions on this node
@@ -261,7 +271,7 @@ module Moxml
261
271
  return [] unless element?
262
272
 
263
273
  adapter.namespace_definitions(@native).map do |ns|
264
- Namespace.new(ns, context)
274
+ Wrappers::Namespace.new(ns, context)
265
275
  end
266
276
  end
267
277
 
@@ -411,7 +421,7 @@ module Moxml
411
421
  # Native equality goes through the adapter: engines with a
412
422
  # native read layer hand out two wrapper classes over one C
413
423
  # node, and raw native == is false across that seam.
414
- self.class == other.class &&
424
+ other.is_a?(Moxml::Node) &&
415
425
  adapter.same_node?(@native, other.native)
416
426
  end
417
427
 
@@ -441,37 +451,6 @@ module Moxml
441
451
  nil
442
452
  end
443
453
 
444
- # Registry mapping node type symbols to wrapper classes.
445
- # Built lazily to avoid load-order issues with subclasses.
446
- def self.node_type_map
447
- @node_type_map ||= {
448
- element: Element,
449
- text: Text,
450
- cdata: Cdata,
451
- comment: Comment,
452
- processing_instruction: ProcessingInstruction,
453
- document: Document,
454
- declaration: Declaration,
455
- doctype: Doctype,
456
- attribute: Attribute,
457
- entity_reference: EntityReference,
458
- }.freeze
459
- end
460
-
461
- def self.wrap(node, context)
462
- return nil if node.nil?
463
-
464
- cached = context.wrapper_for(node)
465
- return cached if cached
466
-
467
- adapter = adapter(context)
468
- type = adapter.node_type(node)
469
- klass = node_type_map[type] || self
470
-
471
- klass.new(node, context, adapter, type)
472
- .tap { |wrapper| context.register_wrapper(node, wrapper) }
473
- end
474
-
475
454
  # Internal: Set the parent node for cache invalidation tracking.
476
455
  # Called by NodeSet, Document, Element when establishing parent-child
477
456
  # relationships. Public to allow cross-class usage within Moxml internals.
@@ -485,12 +464,6 @@ module Moxml
485
464
 
486
465
  protected
487
466
 
488
- def self.adapter(context)
489
- context.config.adapter
490
- end
491
-
492
- # Invalidate cached children. Called by mutation methods
493
- # and by Element attribute/namespace caches.
494
467
  def invalidate_children_cache!
495
468
  @children = nil
496
469
  end
@@ -520,7 +493,7 @@ module Moxml
520
493
 
521
494
  def prepare_node(node)
522
495
  case node
523
- when String then Text.new(adapter.create_text(node), context)
496
+ when String then Moxml::Node.wrap(adapter.create_text(node), context)
524
497
  when Node then node
525
498
  else
526
499
  raise Moxml::DocumentStructureError.new(
@@ -545,4 +518,77 @@ module Moxml
545
518
  xml.gsub(/\r?\n/, line_ending)
546
519
  end
547
520
  end
521
+
522
+ module Node
523
+ include NodeBehavior
524
+
525
+ # Instantiation shells per type — the contract constants
526
+ # (Element, Text, ...) are modules since issue #230, so wrappers
527
+ # mint through Wrappers::*.
528
+ def self.node_type_map
529
+ @node_type_map ||= {
530
+ element: Wrappers::Element,
531
+ text: Wrappers::Text,
532
+ cdata: Wrappers::Cdata,
533
+ comment: Wrappers::Comment,
534
+ processing_instruction: Wrappers::ProcessingInstruction,
535
+ document: Wrappers::Document,
536
+ declaration: Wrappers::Declaration,
537
+ doctype: Wrappers::Doctype,
538
+ attribute: Wrappers::Attribute,
539
+ entity_reference: Wrappers::EntityReference,
540
+ }.freeze
541
+ end
542
+
543
+ # The contract module for a type — what an in-place extended
544
+ # native carries instead of a wrapper shell.
545
+ def self.contract_module(type)
546
+ contract_modules[type]
547
+ end
548
+
549
+ def self.contract_modules
550
+ @contract_modules ||= {
551
+ element: Element,
552
+ text: Text,
553
+ cdata: Cdata,
554
+ comment: Comment,
555
+ processing_instruction: ProcessingInstruction,
556
+ document: Document,
557
+ declaration: Declaration,
558
+ doctype: Doctype,
559
+ attribute: Attribute,
560
+ entity_reference: EntityReference,
561
+ }.freeze
562
+ end
563
+
564
+ def self.wrap(node, context)
565
+ return nil if node.nil?
566
+
567
+ cached = context.wrapper_for(node)
568
+ return cached if cached
569
+
570
+ adapter = adapter(context)
571
+ type = adapter.node_type(node)
572
+
573
+ # Extend-in-place (issue #230): adapters whose natives can
574
+ # carry the contract modules directly (leptris TypedData) mint
575
+ # the native itself as the wrapper — @native is self.
576
+ if (extended = adapter.wrap_native(node, type, context))
577
+ extended.prime_contract(node, context, adapter, type)
578
+ context.register_wrapper(node, extended)
579
+ return extended
580
+ end
581
+
582
+ klass = node_type_map[type] || Wrappers::Node
583
+ klass.new(node, context, adapter, type)
584
+ .tap { |wrapper| context.register_wrapper(node, wrapper) }
585
+ end
586
+
587
+ def self.adapter(context)
588
+ context.config.adapter
589
+ end
590
+
591
+ # Invalidate cached children. Called by mutation methods
592
+ # and by Element attribute/namespace caches.
593
+ end
548
594
  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.46"
4
+ VERSION = "0.5.48"
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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.46
4
+ version: 0.5.48
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -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