moxml 0.5.70 → 0.5.71

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: 5b329e431ed88f56890e49f855af38ed25c7ac216cbb35fe91dfc1377deeb44a
4
- data.tar.gz: 113f2b97ceae1f8fcd3bb557ada4205c693ce8e603078ca2b749f5cbb91f0f86
3
+ metadata.gz: 20275fa1b50e6428f448b919513cea0b71c6737e002254bd34802f2491f4a6df
4
+ data.tar.gz: '09f7c98af52d5efab3a8ca2efbee63a50f3eae2c26cb1a8f1d4795711462c6b6'
5
5
  SHA512:
6
- metadata.gz: b7de514485b6b9bb2393f61584b6f02d474bcd351ed2dc049aebc29ed31c4f8d2220b80facb9757df43fcac0bf8a0a46023c09af7c987ad06b22eaceea181bad
7
- data.tar.gz: c3cd5913e50a4b700609cee9729e1eeb1b2eddcd98292a83764268319f2dcd0f8c79092459f58deba10f9a9eeec4de6a467422316b11496148fc0ab26edcee7c
6
+ metadata.gz: 4474310fe16f301dacbef2e6eac14f1b7cbb6c3be75fd547ed6ea85e054e0ad8c6fd60d3d11448e17ee2d556408670a1999ed8cb1d4dec83392480d6b6283e63
7
+ data.tar.gz: 7167a5ef4c392461ecb813ca330e1800f2433ed9c3254925943d921223210e84d480826ee8787728a14cb29c2f3b0f3f4f2c3141b3b87a4bc1d3df3e204798c5
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.194"
13
13
  gem "libxml-ruby", ">= 5.0"
14
14
  gem "nokogiri", "~> 1.18"
15
15
  gem "openssl", "~> 3.0"
data/README.adoc CHANGED
@@ -91,8 +91,10 @@ moxml's customized generator and decoder overrides.
91
91
  6.0 itself requires Ruby >= 3.2); Ruby 3.1 resolves 5.x.
92
92
 
93
93
  |Leptris (leptris-ruby)
94
- |>= 1.9.32
94
+ |>= 1.9.194.1
95
95
  |Hard floor — `MINIMUM_BINDING_VERSION`; older bindings do not load.
96
+ Raised from 1.9.32 (2026-09-20): every known consumer resolves a
97
+ modern binding, and the sub-1.9.194 accommodation paths are deleted.
96
98
  Newer bindings unlock more native surface automatically:
97
99
  1.9.163.7+ single-result XPath seams, 1.9.174.4+ C-minted identity
98
100
  wrappers, 1.9.174.6+ one-dispatch subtree walks, 1.9.177+ first-class
@@ -358,6 +358,20 @@ namespace_validation_mode: :strict)
358
358
  nil
359
359
  end
360
360
 
361
+ # Read-only attribute listing as [name, value] pairs
362
+ # (Moxml::Element#attribute_pairs) — document order,
363
+ # duplicates included, no Attribute node wrappers. The
364
+ # walk-hot shape for consumers that only read name/value;
365
+ # mutation and per-attribute namespace resolution stay on
366
+ # #attributes. This default derives from #attributes;
367
+ # adapters with a bulk face override (leptris answers in
368
+ # one C crossing — leptris-ruby#278).
369
+ def attribute_pairs(element)
370
+ attributes(element).map do |attr|
371
+ [attribute_name(attr), attr.value.to_s]
372
+ end
373
+ end
374
+
361
375
  # Plan row stream (Moxml::Plan) — ADAPTER CONTRACT. Yields
362
376
  # |name, attrs_pairs (flat [k, v, ...]), first-text, depth|
363
377
  # per element in document order (pre-order); first-text is
@@ -26,18 +26,17 @@ module Moxml
26
26
  # (1.7.0) surfaces predate it. Older bindings are not eligible
27
27
  # for the default (see Config.leptris_preferred_available?) and
28
28
  # the adapter no longer carries accommodation paths for them.
29
- MINIMUM_BINDING_VERSION = "1.9.32"
29
+ # Raised 1.9.32 -> 1.9.194.1 (2026-09-20): every known consumer
30
+ # resolves a modern binding (canon itself pins ~> 1.9.193), and
31
+ # the sub-1.9.194 accommodation paths are deleted.
32
+ MINIMUM_BINDING_VERSION = "1.9.194.1"
30
33
 
31
34
  # leptris_parse_html_string shipped in bindings 1.9.80
32
35
  # (libleptris 1.9.75, engine #659) as Leptris::XML.parse_html.
33
- HTML_PARSE_SUPPORTED =
34
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.80")
35
-
36
36
  # Attribute-node xpath results carry proper wrappers since
37
37
  # 1.9.105 (leptris-ruby#153: ResultAttr with name/value);
38
38
  # before that the native gate routed them to the Ruby engine.
39
- ATTR_RESULT_NATIVE =
40
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.105")
39
+ ATTR_RESULT_NATIVE = true
41
40
 
42
41
  # leptris_node_digest shipped in bindings 1.9.99 (libleptris
43
42
  # 1.9.99, engine #869): on-demand Merkle subtree hash, zero
@@ -223,77 +222,46 @@ module Moxml
223
222
  ELEMENT_CONTRACT_READ =
224
223
  ::Moxml::ElementBehavior.instance_method(:[])
225
224
 
226
- NATIVE_HOT_ALIASES =
227
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.194.1")
228
-
229
- # Doc-level entity-marker memo shared by both hot-read
230
- # faces (one WeakMap, generation-stamped Integer values).
225
+ # Bindings >= 1.9.194.1 (the floor) carry unshadowed
226
+ # aliases; the UnboundMethod#bind_call face for older
227
+ # shapes is deleted.
228
+ # Doc-level entity-marker memo (one WeakMap,
229
+ # generation-stamped Integer values).
231
230
  ENTITY_DOC_MEMO = ::ObjectSpace::WeakMap.new
232
231
 
233
- if NATIVE_HOT_ALIASES
234
- module Reads
235
- # Marker presence is a DOCUMENT fact, but the wrapper
236
- # memo re-derives it per node (the per-read machinery
237
- # was ~a quarter of the consumer walk). Doc-level
238
- # WeakMap, generation-stamped into one Integer
239
- # (immediates make safe weak values): one C document
240
- # read plus one map hit per bare read. Entries die
241
- # with their documents; a stale generation re-derives.
242
- def doc_entity_bearing?
243
- doc = NN_DOCUMENT.bind_call(self)
244
- memo = ENTITY_DOC_MEMO[doc]
245
- gen = ::Moxml::Adapter::Leptris.serialize_generation
246
- return memo.allbits?(1) if memo && (memo >> 1) == gen
247
-
248
- bearing = ::Moxml::Adapter::Leptris.entity_bearing?(self)
249
- ENTITY_DOC_MEMO[doc] = (gen << 1) | (bearing ? 1 : 0)
250
- bearing
251
- end
252
-
253
- def [](key)
254
- if key.is_a?(String) && !key.include?(":")
255
- value = attr_read(key)
256
- return value unless value.is_a?(String) && doc_entity_bearing?
232
+ module Reads
233
+ # Marker presence is a DOCUMENT fact, but the wrapper
234
+ # memo re-derives it per node (the per-read machinery
235
+ # was ~a quarter of the consumer walk). Doc-level
236
+ # WeakMap, generation-stamped into one Integer
237
+ # (immediates make safe weak values): one C document
238
+ # read plus one map hit per bare read. Entries die
239
+ # with their documents; a stale generation re-derives.
240
+ def doc_entity_bearing?
241
+ doc = NN_DOCUMENT.bind_call(self)
242
+ memo = ENTITY_DOC_MEMO[doc]
243
+ gen = ::Moxml::Adapter::Leptris.serialize_generation
244
+ return memo.allbits?(1) if memo && (memo >> 1) == gen
245
+
246
+ bearing = ::Moxml::Adapter::Leptris.entity_bearing?(self)
247
+ ENTITY_DOC_MEMO[doc] = (gen << 1) | (bearing ? 1 : 0)
248
+ bearing
249
+ end
257
250
 
258
- return ::Moxml::Adapter::Leptris.restore_entities(value)
259
- end
251
+ def [](key)
252
+ if key.is_a?(String) && !key.include?(":")
253
+ value = attr_read(key)
254
+ return value unless value.is_a?(String) && doc_entity_bearing?
260
255
 
261
- ELEMENT_CONTRACT_READ.bind_call(self, key)
256
+ return ::Moxml::Adapter::Leptris.restore_entities(value)
262
257
  end
263
258
 
264
- def text
265
- value = text_read
266
- value.is_a?(String) && doc_entity_bearing? ? ::Moxml::Adapter::Leptris.restore_entities(value) : value
267
- end
259
+ ELEMENT_CONTRACT_READ.bind_call(self, key)
268
260
  end
269
- else
270
- module Reads
271
- def doc_entity_bearing?
272
- doc = NN_DOCUMENT.bind_call(self)
273
- memo = ENTITY_DOC_MEMO[doc]
274
- gen = ::Moxml::Adapter::Leptris.serialize_generation
275
- return memo.allbits?(1) if memo && (memo >> 1) == gen
276
-
277
- bearing = ::Moxml::Adapter::Leptris.entity_bearing?(self)
278
- ENTITY_DOC_MEMO[doc] = (gen << 1) | (bearing ? 1 : 0)
279
- bearing
280
- end
281
-
282
- def [](key)
283
- if key.is_a?(String) && !key.include?(":")
284
- value = NN_ATTRIBUTE.bind_call(self, key)
285
- return value unless value.is_a?(String) && doc_entity_bearing?
286
-
287
- return ::Moxml::Adapter::Leptris.restore_entities(value)
288
- end
289
261
 
290
- ELEMENT_CONTRACT_READ.bind_call(self, key)
291
- end
292
-
293
- def text
294
- value = NN_CONTENT.bind_call(self)
295
- value.is_a?(String) && doc_entity_bearing? ? ::Moxml::Adapter::Leptris.restore_entities(value) : value
296
- end
262
+ def text
263
+ value = text_read
264
+ value.is_a?(String) && doc_entity_bearing? ? ::Moxml::Adapter::Leptris.restore_entities(value) : value
297
265
  end
298
266
  end
299
267
  ELEMENT.include(Reads)
@@ -536,9 +504,7 @@ module Moxml
536
504
  # exactly the plan's shape, with no wrapper minting at all.
537
505
  # Marker-bearing documents stay on the generic path (the bulk
538
506
  # stream has no marker split).
539
- NATIVE_PLAN_ROWS =
540
- NATIVE_READ_LAYER &&
541
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.193.4")
507
+ NATIVE_PLAN_ROWS = true
542
508
 
543
509
  # leptris-ruby#272 (binding 1.9.201.1): Native.plan_structs —
544
510
  # the C struct executor behind Moxml::StructPlan. The floor
@@ -602,20 +568,10 @@ module Moxml
602
568
  end
603
569
  return nil unless root_binding
604
570
 
605
- rows = if NATIVE_PLAN_ROWS
606
- ::Leptris::XML::Native.snapshot_rows(doc,
607
- root_binding.c_address)
608
- else
609
- doc.snapshot(root_binding)
610
- end
571
+ rows = ::Leptris::XML::Native.snapshot_rows(doc,
572
+ root_binding.c_address)
611
573
  rows.each do |row|
612
- if row.is_a?(::Array)
613
- yield(row[0], row[1], row[2], row[3])
614
- else
615
- next unless row[:kind] == "element"
616
-
617
- yield(row[:name], row[:attrs].flatten, row[:text], row[:depth])
618
- end
574
+ yield(row[0], row[1], row[2], row[3])
619
575
  end
620
576
  true
621
577
  end
@@ -806,13 +762,6 @@ module Moxml
806
762
  end
807
763
 
808
764
  def parse_html(html, _options = {}, _context = nil)
809
- unless HTML_PARSE_SUPPORTED
810
- raise Moxml::AdapterError.new(
811
- "HTML parsing requires leptris >= 1.9.80 (have #{::Leptris::VERSION})",
812
- adapter: name, operation: "parse_html",
813
- )
814
- end
815
-
816
765
  html_string = html.is_a?(IO) || html.is_a?(StringIO) ? html.read : html.to_s
817
766
  native_doc = begin
818
767
  ::Leptris::XML.parse_html(html_string)
@@ -1460,6 +1409,18 @@ module Moxml
1460
1409
  element.attribute_nodes
1461
1410
  end
1462
1411
 
1412
+ # One C crossing per element on bindings with the bulk face
1413
+ # (leptris-ruby#278): interned names, fresh values, zero
1414
+ # Attr objects. Face-probed, not version-gated — the floor
1415
+ # (1.9.194.1) admits bindings without it, which take the
1416
+ # base default over #attributes.
1417
+ def attribute_pairs(element)
1418
+ binding_element = to_binding(element)
1419
+ return super unless binding_element.respond_to?(:attribute_pairs)
1420
+
1421
+ binding_element.attribute_pairs
1422
+ end
1423
+
1463
1424
  def attribute_element(attr)
1464
1425
  attr.element
1465
1426
  end
data/lib/moxml/element.rb CHANGED
@@ -115,6 +115,17 @@ module Moxml
115
115
  self[attr_name]
116
116
  end
117
117
 
118
+ # Read-only [name, value] pairs in document order, duplicates
119
+ # included — no Attribute node wrappers. The walk-hot shape for
120
+ # consumers that only read name/value (leptris-ruby#278): on
121
+ # leptris >= 1.9.208.1 this answers in one C crossing with zero
122
+ # per-attribute objects. Names may be shared frozen strings —
123
+ # dup before mutating. Mutation and per-attribute namespace
124
+ # resolution stay on #attributes.
125
+ def attribute_pairs
126
+ adapter.attribute_pairs(@native)
127
+ end
128
+
118
129
  def attributes
119
130
  # Primed like Node.wrap: the adapter and type are known at
120
131
  # mint, so the first name/value/attribute? access skips 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.70"
4
+ VERSION = "0.5.71"
5
5
  end
@@ -324,10 +324,8 @@ RSpec.describe Moxml::Adapter::Leptris do
324
324
  end
325
325
 
326
326
  describe "HTML parsing (leptris/leptris#659)" do
327
- before do
328
- skip "requires leptris 1.9.80+ (HTML engine mode)" unless described_class::HTML_PARSE_SUPPORTED
329
- end
330
-
327
+ # leptris >= 1.9.80 is below the binding floor (1.9.194.1) —
328
+ # the HTML engine mode is always present.
331
329
  let(:ctx) { Moxml.new(:leptris) }
332
330
 
333
331
  it "synthesizes the html/body structure with lowercased names" do
@@ -93,11 +93,11 @@ RSpec.describe Moxml::Adapter do
93
93
  end
94
94
  end
95
95
 
96
- it "pins the leptris binding floor at 1.9.32 (issue #149)" do
96
+ it "pins the leptris binding floor at 1.9.194.1 (issue #149; raised 2026-09-20)" do
97
97
  skip "leptris not installed" unless described_class.available?(:leptris)
98
98
 
99
99
  adapter = described_class.load(:leptris)
100
- expect(adapter::MINIMUM_BINDING_VERSION).to eq("1.9.32")
100
+ expect(adapter::MINIMUM_BINDING_VERSION).to eq("1.9.194.1")
101
101
  expect(Gem::Version.new(Leptris::VERSION))
102
102
  .to be >= Gem::Version.new(adapter::MINIMUM_BINDING_VERSION)
103
103
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # Element#attribute_pairs adapter-contract conformance
6
+ # (leptris-ruby#278): pairs must equal what the node path yields —
7
+ # attributes.map { |a| [a.name, a.value.to_s] } — on every adapter,
8
+ # whether the adapter answers through a bulk C face (leptris >=
9
+ # 1.9.208.1) or derives from #attributes.
10
+ RSpec.describe Moxml::Element do
11
+ let(:xml) do
12
+ '<catalog><record id="r0" kind="k0"><field name="f0" unit="u0">v</field></record></catalog>'
13
+ end
14
+
15
+ ADAPTERS = %i[leptris nokogiri ox oga rexml libxml].freeze # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
16
+
17
+ def with_adapter(name)
18
+ ctx = Moxml.new(name)
19
+ yield ctx
20
+ rescue StandardError, LoadError
21
+ skip "adapter #{name} unavailable"
22
+ end
23
+
24
+ it "matches the node path on every available adapter" do
25
+ results = ADAPTERS.filter_map do |name|
26
+ with_adapter(name) do |ctx|
27
+ root = ctx.parse(xml).root
28
+ [name, root.attribute_pairs]
29
+ end
30
+ end
31
+ expect(results).not_to be_empty
32
+
33
+ results.each do |name, pairs|
34
+ with_adapter(name) do |ctx|
35
+ root = ctx.parse(xml).root
36
+ node_derived = root.attributes.map { |a| [a.name, a.value.to_s] }
37
+ expect(pairs).to eq(node_derived),
38
+ "#{name}: #{pairs.inspect} vs #{node_derived.inspect}"
39
+ end
40
+ end
41
+ end
42
+ end
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.70
4
+ version: 0.5.71
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-19 00:00:00.000000000 Z
11
+ date: 2026-09-20 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
@@ -466,6 +466,7 @@ files:
466
466
  - spec/moxml/document_builder_spec.rb
467
467
  - spec/moxml/document_free_spec.rb
468
468
  - spec/moxml/document_spec.rb
469
+ - spec/moxml/element_attribute_pairs_spec.rb
469
470
  - spec/moxml/element_spec.rb
470
471
  - spec/moxml/entity_preservation_spec.rb
471
472
  - spec/moxml/entity_reference_spec.rb