moxml 0.5.22 → 0.5.24

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: 9d5a6ef9ddc86140c162e868d05587e8e70552bcfb17f7964024063856b60b5f
4
- data.tar.gz: 664be87c9b0e0b00757a696ae49d7c97a67994bc225fbde8e224c20a642133ba
3
+ metadata.gz: b1181803fec34397cfd858d8671e2dcd6a02a88418dc5848a5ea9c7a44d7bc64
4
+ data.tar.gz: b9809e6ed48ae4a5a946ed9487fe61d0a89d333b51921de70b50dd271956ba0a
5
5
  SHA512:
6
- metadata.gz: 8f9927536fa366b63c1673a7f7721ec772e0964f653d2c4c71861e58ddfc3d3c46855fe1fc288471045357416e40f908de9c60f1b2adf12849e44438f60df22b
7
- data.tar.gz: 7cf7c5a05eed212702b2a58b18e4deecae8e474e2f9ff74483188fc353fec8320465695e843f8aec0663f9e67130b6f5fa66b01992b6ac1e903dc88e35f3000b
6
+ metadata.gz: 26df2786fe6a81b696fe2273c2d5f36151bb6af9daf2ce10f057662f7df3ce8ca75970f883a832dbc0c4741e7262be87d79a6eac143298dc9c398ba198f6ba43
7
+ data.tar.gz: d7d155e11b4d1718e072ad5d2eabcb3aad8c2875753eea571bde7e710deee1c88a8acb3244cb01b4055b4c7e648384f91410c42ad576d600ab4c49ffcb39b895
@@ -71,9 +71,8 @@ implementation over one `leptris_node_traverse` call.
71
71
 
72
72
  * Tab indentation honors the indent-unit string on leptris >= 1.9.45
73
73
  (`indent_text:`), byte-identical to Nokogiri including
74
- text-bearing children since the engine fix (leptris/leptris#658,
75
- 1.9.46); comment children still diverge on the element unit path
76
- (leptris-ruby#115).
74
+ text-bearing children and comment children alike
75
+ (leptris/leptris#658 at 1.9.46; leptris-ruby#115 at 1.9.50).
77
76
  * A PI preceding the DOCTYPE reorders on serialization — libleptris
78
77
  normalizes DOCTYPE-first and the document node does not record its
79
78
  relative position.
@@ -147,10 +147,11 @@ module Moxml
147
147
  end
148
148
 
149
149
  # The engine's subset serializer mangles every declaration
150
- # after the first (loses its "<") — moxml's own formatter is
151
- # correct, so multi-declaration subsets compose.
150
+ # after the first (leptris/leptris#687) — moxml's own
151
+ # formatter is correct, so those compose until the probe
152
+ # says the engine is fixed.
152
153
  dt = doc.doctype
153
- if dt&.class&.method_defined?(:internal_subset) &&
154
+ if !ENGINE_MULTI_DECL_SUBSET_OK && dt&.class&.method_defined?(:internal_subset) &&
154
155
  (subset = dt.internal_subset) && subset.scan("<!").size > 1
155
156
  return nil
156
157
  end
@@ -75,27 +75,34 @@ module Moxml
75
75
  # Element output always ends with the close tag — but the
76
76
  # engine's serializer appends a stray trailing newline when
77
77
  # the element's last text child is non-ASCII (fixed engine
78
- # side in 1.9.42; kept for older floor bindings). Note: the
79
- # binding's indent_text kwarg is the display-form boolean,
80
- # not Nokogiri's indent-unit string — do not forward it.
78
+ # side in 1.9.42; kept for older floor bindings).
81
79
  xml.sub(/\n+\z/, "")
82
80
  end
83
81
 
82
+ # A bare ampersand — not starting a named or numeric entity
83
+ # reference. Some engine builds (observed: the Linux 1.9.50
84
+ # platform gem) emit text-content ampersands unescaped; the
85
+ # detection below is a no-op on correct builds.
86
+ RAW_AMP_RE = /&(?!#{Entity::NAME_PATTERN};|#\d+;|#x[0-9A-Fa-f]+;)/
87
+
84
88
  def normalize_serialization(xml, options)
85
- needs_apos = xml.include?("&apos;")
89
+ # The libxml2-layout serializer (>= 1.9.42) keeps attribute
90
+ # apostrophes literal; older engines escaped them.
91
+ needs_apos = !LIBXML2_LAYOUT_PARITY && xml.include?("&apos;")
86
92
  needs_expand = options[:expand_empty] && xml.include?("/>")
87
- return xml unless needs_apos || needs_expand
93
+ needs_amp = xml.include?("&") && xml.match?(RAW_AMP_RE)
94
+ return xml unless needs_apos || needs_expand || needs_amp
88
95
 
89
96
  out = +""
90
97
  pos = 0
91
98
  while pos < xml.length
92
99
  opener_at, terminator = next_literal_region(xml, pos)
93
100
  if opener_at.nil?
94
- out << normalize_markup(xml[pos..], needs_apos, needs_expand)
101
+ out << normalize_markup(xml[pos..], needs_apos, needs_expand, needs_amp: needs_amp)
95
102
  break
96
103
  end
97
104
 
98
- out << normalize_markup(xml[pos...opener_at], needs_apos, needs_expand)
105
+ out << normalize_markup(xml[pos...opener_at], needs_apos, needs_expand, needs_amp: needs_amp)
99
106
  search_from = opener_at + opener_at_offset(terminator)
100
107
  close = xml.index(terminator, search_from)
101
108
  close_end = close.nil? ? xml.length : close + terminator.length
@@ -127,13 +134,16 @@ module Moxml
127
134
  terminator == "-->" ? 4 : 0
128
135
  end
129
136
 
130
- def normalize_markup(markup, needs_apos, needs_expand)
137
+ def normalize_markup(markup, needs_apos, needs_expand, needs_amp: false)
131
138
  markup = markup.gsub("&apos;", "'") if needs_apos
132
139
  if needs_expand
133
140
  markup = markup.gsub(EMPTY_ELEMENT_RE) do
134
141
  "<#{Regexp.last_match(1)}#{Regexp.last_match(2)}></#{Regexp.last_match(1)}>"
135
142
  end
136
143
  end
144
+ # Segment-aware: this never sees CDATA/comment/PI content,
145
+ # where a bare & is literal data.
146
+ markup = markup.gsub(RAW_AMP_RE, "&amp;") if needs_amp
137
147
  markup
138
148
  end
139
149
  end
@@ -49,6 +49,39 @@ module Moxml
49
49
  INDENT_UNIT_SUPPORTED =
50
50
  Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.45")
51
51
 
52
+ # leptris-ruby#115 (fixed 1.9.50): the element unit path keeps
53
+ # child comments.
54
+ ELEMENT_UNIT_COMMENTS =
55
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.50")
56
+
57
+ # leptris/leptris#677: the engine's DROP_WS_TEXT trims boundary
58
+ # whitespace of non-blank text nodes. Probed at load rather
59
+ # than version-gated so the C fix is adopted the moment a fixed
60
+ # binding installs — no moxml release needed. While the probe
61
+ # fails, noblanks drops blanks moxml-side after parse.
62
+ ENGINE_NOBLANKS_SAFE = begin
63
+ doc = ::Leptris::XML::Document.parse(
64
+ "<r> x</r>", options: ::Leptris::XML::ParseOptions.noblanks
65
+ )
66
+ text = doc.root.children.first
67
+ text.is_a?(::Leptris::XML::Text) && text.content == " x"
68
+ rescue StandardError
69
+ false
70
+ end
71
+
72
+ # leptris/leptris#687: the engine's subset serializer mangles
73
+ # every internal-subset declaration after the first. Probed for
74
+ # the same reason — once fixed, the whole-document fast path
75
+ # serves multi-declaration subsets too.
76
+ ENGINE_MULTI_DECL_SUBSET_OK = begin
77
+ doc = ::Leptris::XML::Document.parse(
78
+ %(<?xml version="1.0"?><!DOCTYPE r [<!ELEMENT r (#PCDATA)><!ATTLIST e a CDATA "d">]><r/>),
79
+ )
80
+ doc.to_xml.include?("<!ATTLIST")
81
+ rescue StandardError
82
+ false
83
+ end
84
+
52
85
  NO_PARSE_ERRORS = [].freeze
53
86
  private_constant :NO_PARSE_ERRORS
54
87
 
@@ -106,10 +139,10 @@ module Moxml
106
139
  ctx = _context || Context.new(:leptris)
107
140
  doc = Document.new(native_doc, ctx)
108
141
 
109
- if options[:noblanks]
142
+ if options[:noblanks] && !ENGINE_NOBLANKS_SAFE
110
143
  # The engine's DROP_WS_TEXT trims boundary whitespace of
111
144
  # NON-blank text nodes at parse (leptris/leptris#677), so
112
- # the flag is never forwarded — blanks drop here instead.
145
+ # the flag is not forwarded — blanks drop here instead.
113
146
  if options[:readonly] == true
114
147
  raise ArgumentError,
115
148
  "noblanks: true requires a mutable document (readonly: true freezes the tree at parse)"
@@ -128,14 +161,12 @@ module Moxml
128
161
  # nil when no parse flag is requested — the binding treats a
129
162
  # nil options hash as plain defaults (matching libxml2/
130
163
  # Nokogiri semantics: blanks kept, no ATTLIST defaults).
131
- # noblanks is deliberately absent: the engine's
132
- # LEPTRIS_PARSE_DROP_WS_TEXT trims boundary whitespace of
133
- # non-blank text nodes too (leptris/leptris#677), so moxml
134
- # implements it itself after parse — see
135
- # strip_blank_text_nodes!.
164
+ # noblanks forwards only once the engine flag is libxml2-safe
165
+ # (see ENGINE_NOBLANKS_SAFE); otherwise it is moxml-side.
136
166
  def parse_flags(options)
137
167
  flags = 0
138
168
  flags |= ::Leptris::XML::ParseOptions::DTDATTR if options[:dtdattr] == true
169
+ flags |= ::Leptris::XML::ParseOptions::NOBLANKS if options[:noblanks] == true && ENGINE_NOBLANKS_SAFE
139
170
  flags.zero? ? nil : ::Leptris::XML::ParseOptions.new(flags)
140
171
  end
141
172
 
@@ -10,6 +10,7 @@ module Moxml
10
10
  # Mutation return contract (Adapter::Base): returns the native
11
11
  # to keep tracking — same object for in-place adapters, fresh
12
12
  # object for value-object adapters (leptris).
13
+ context.bump_namespace_scope_generation
13
14
  @native = adapter.set_attribute_name(@native, new_name)
14
15
  end
15
16
 
@@ -32,6 +33,7 @@ module Moxml
32
33
  end
33
34
 
34
35
  def value=(new_value)
36
+ context.bump_namespace_scope_generation
35
37
  adapter.set_attribute_value(@native, new_value)
36
38
  end
37
39
 
data/lib/moxml/element.rb CHANGED
@@ -3,10 +3,14 @@
3
3
  module Moxml
4
4
  class Element < Node
5
5
  def name
6
- adapter.node_name(@native)
6
+ # The adapter read is an FFI call plus (on several adapters) a
7
+ # defensive string copy; names only change through name= and
8
+ # native adoption, both of which clear this.
9
+ @name ||= adapter.node_name(@native)
7
10
  end
8
11
 
9
12
  def name=(value)
13
+ @name = nil
10
14
  adapter.set_node_name(@native, value)
11
15
  end
12
16
 
@@ -42,6 +46,7 @@ module Moxml
42
46
  # attributes only; prefixed names replace by namespace URI and
43
47
  # require a declared prefix. Cache coherence is the resolver's.
44
48
  def []=(name, value)
49
+ context.bump_namespace_scope_generation
45
50
  Moxml::AttributeResolver.assign(self, name, normalize_xml_value(value))
46
51
  end
47
52
 
@@ -49,13 +54,30 @@ module Moxml
49
54
  # bare names match only no-namespace attributes; prefixed names
50
55
  # resolve through in-scope declarations to expanded names.
51
56
  def [](name)
52
- Moxml::AttributeResolver.resolve(self, name)&.value
57
+ cache = attribute_read_cache
58
+ key = name.to_s
59
+ unless cache.key?(key)
60
+ cache[key] = Moxml::AttributeResolver.resolve(self, key)
61
+ end
62
+ cache[key]&.value
53
63
  end
54
64
 
55
65
  def attribute(name)
56
66
  Moxml::AttributeResolver.resolve(self, name)
57
67
  end
58
68
 
69
+ # Resolution reads the element.s in-scope namespaces, so the
70
+ # cache rides the context.s namespace-scope generation clock —
71
+ # every attribute or namespace mutation anywhere bumps it.
72
+ def attribute_read_cache
73
+ generation = context.namespace_scope_generation
74
+ if @attribute_cache_generation != generation
75
+ @attribute_cache = {}
76
+ @attribute_cache_generation = generation
77
+ end
78
+ @attribute_cache
79
+ end
80
+
59
81
  # Returns attribute value by name (used by XPath engine)
60
82
  def get(attr_name)
61
83
  self[attr_name]
@@ -70,6 +92,7 @@ module Moxml
70
92
  end
71
93
 
72
94
  def remove_attribute(name)
95
+ context.bump_namespace_scope_generation
73
96
  Moxml::AttributeResolver.remove(self, name)
74
97
  self
75
98
  end
@@ -223,9 +246,13 @@ module Moxml
223
246
  children
224
247
  end
225
248
 
226
- # Called by Attribute#remove to invalidate the cached attributes
249
+ # Called by Attribute#remove and the attribute mutators. Clears
250
+ # the attribute list and the resolved-read cache, and bumps the
251
+ # context generation so cross-wrapper reads recompute.
227
252
  def invalidate_attribute_cache!
228
253
  @attributes = nil
254
+ @attribute_cache = nil
255
+ context.bump_namespace_scope_generation
229
256
  end
230
257
 
231
258
  # Clear the namespace caches and bump the context's scope
data/lib/moxml/node.rb CHANGED
@@ -25,10 +25,18 @@ module Moxml
25
25
  context.unregister_wrapper(@native)
26
26
  @native = new_native
27
27
  context.register_wrapper(new_native, self)
28
+ clear_native_memo!
28
29
  end
29
30
  self
30
31
  end
31
32
 
33
+ # Wrapper-local memos derived from the native (name, attribute
34
+ # reads) die with the native they were computed from.
35
+ def clear_native_memo!
36
+ @name = nil
37
+ @attribute_cache = nil
38
+ end
39
+
32
40
  def document
33
41
  Document.wrap(adapter.document(@native), context)
34
42
  end
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.22"
4
+ VERSION = "0.5.24"
5
5
  end
@@ -175,6 +175,14 @@ RSpec.describe Moxml::Adapter::Leptris do
175
175
  end
176
176
  end
177
177
 
178
+ it "keeps comments in element serialization with tab units (leptris-ruby#115)" do
179
+ skip "requires the element unit fix (leptris 1.9.50+)" unless described_class::ELEMENT_UNIT_COMMENTS
180
+
181
+ doc = ctx.parse(%(<r><a/><!-- c --></r>))
182
+ expect(doc.root.to_xml(indent: 1, indent_text: "\t"))
183
+ .to eq("<r>\n\t<a></a>\n\t<!-- c -->\n</r>")
184
+ end
185
+
178
186
  it "round-trips document-level PI mutations through serialization" do
179
187
  # leptris/leptris#612: parse-created document PIs carry doc
180
188
  # linkage — the setters work and the tree round-trips.
@@ -235,8 +243,12 @@ RSpec.describe Moxml::Adapter::Leptris do
235
243
  expect(doc.root.children.to_a.select(&:text?)).to be_empty
236
244
  end
237
245
 
238
- it "refuses noblanks on readonly parses" do
239
- # The strip mutates the tree; readonly freezes it at parse.
246
+ it "refuses noblanks on readonly parses while the strip path is active" do
247
+ # The strip mutates the tree; readonly freezes it at parse. On
248
+ # bindings whose engine flag is libxml2-safe (probe), the flag
249
+ # forwards at parse and no mutability is needed.
250
+ skip "engine noblanks flag is libxml2-safe — no strip path" if described_class::ENGINE_NOBLANKS_SAFE
251
+
240
252
  expect { ctx.parse("<a/>", noblanks: true, readonly: true) }
241
253
  .to raise_error(ArgumentError, /noblanks.*mutable/)
242
254
  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.22
4
+ version: 0.5.24
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-01 00:00:00.000000000 Z
11
+ date: 2026-09-02 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