moxml 0.5.26 → 0.5.28

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: b7d4ecc2bb794f60a6b240faf3828c7847da9eebf8d45e44b35dd863f8ef7b09
4
- data.tar.gz: 04d2ecbc979863a35825a8ba95a7897d4f7d395629c5ca959dfa8fd96c71a662
3
+ metadata.gz: c393ef51976af7e0ccf0ff232d009863a0b2b7be044b70e8500beedb216bd122
4
+ data.tar.gz: 1718f58a399c09fc218b269faa62dbd52e50e2bbdcdea32e52f9c7e72ad05a8a
5
5
  SHA512:
6
- metadata.gz: a63b4c25f0838edbb6ba6cb787b68000e614afbfbb700a3167a5222ab7219fef4f7add25a009991de44b78eeb2b117c96f0743655e0fa058e4ac3d697877093b
7
- data.tar.gz: c8700770e6873e0a9af730cc94b149c019d63df0aadd585f3f382e1c4fe4246d9d4bb243e9f8347ed4bdb83b67d84aa497de69152cf3ea2921c6be55b9037d05
6
+ metadata.gz: cf11cb89af1acd465fe769616d925e1c20fb3961953ed29483147ef67a132e09a52f763601a3397887f6ff49d42f4dddc2b27ce6f1f659d99062e332eb401cf2
7
+ data.tar.gz: 5033d59e77e627b40fb054036ac87ee45e5c60408e37dcb6ff2790cca7a3276f4a45ee62d36bab5ec2d9d7af038eb4c1467c84bd3174ace1b5fc73b483d0d8db
@@ -85,26 +85,37 @@ module Moxml
85
85
  # detection below is a no-op on correct builds.
86
86
  RAW_AMP_RE = /&(?!#{Entity::NAME_PATTERN};|#\d+;|#x[0-9A-Fa-f]+;)/
87
87
 
88
+ # A raw "<" in text position — the engine intermittently
89
+ # loses the escape when parsing under allocation pressure
90
+ # (leptris-ruby#131), so a decoded &#x3c; serializes bare and
91
+ # reparsing truncates at it. Valid markup NEVER puts "<"
92
+ # before a non-name, non-delimiter character, so this is
93
+ # deterministic in markup segments (CDATA is literal and
94
+ # excluded by the segment walker).
95
+ RAW_LT_RE = /<(?![A-Za-z_:\/?!])/
96
+
88
97
  def normalize_serialization(xml, options)
89
98
  # The libxml2-layout serializer (>= 1.9.42) keeps attribute
90
99
  # apostrophes literal; older engines escaped them.
91
100
  needs_apos = !LIBXML2_LAYOUT_PARITY && xml.include?("&apos;")
92
101
  needs_expand = options[:expand_empty] && xml.include?("/>")
93
- # One scan: the bare-ampersand regex fails fast on
94
- # ampersand-free output — no include? pre-filter needed.
102
+ # One scan each: both regexes fail fast on clean output.
95
103
  needs_amp = xml.match?(RAW_AMP_RE)
96
- return xml unless needs_apos || needs_expand || needs_amp
104
+ needs_lt = xml.match?(RAW_LT_RE)
105
+ return xml unless needs_apos || needs_expand || needs_amp || needs_lt
97
106
 
98
107
  out = +""
99
108
  pos = 0
100
109
  while pos < xml.length
101
110
  opener_at, terminator = next_literal_region(xml, pos)
102
111
  if opener_at.nil?
103
- out << normalize_markup(xml[pos..], needs_apos, needs_expand, needs_amp: needs_amp)
112
+ out << normalize_markup(xml[pos..], needs_apos, needs_expand,
113
+ needs_amp: needs_amp, needs_lt: needs_lt)
104
114
  break
105
115
  end
106
116
 
107
- out << normalize_markup(xml[pos...opener_at], needs_apos, needs_expand, needs_amp: needs_amp)
117
+ out << normalize_markup(xml[pos...opener_at], needs_apos, needs_expand,
118
+ needs_amp: needs_amp, needs_lt: needs_lt)
108
119
  search_from = opener_at + opener_at_offset(terminator)
109
120
  close = xml.index(terminator, search_from)
110
121
  close_end = close.nil? ? xml.length : close + terminator.length
@@ -136,7 +147,7 @@ module Moxml
136
147
  terminator == "-->" ? 4 : 0
137
148
  end
138
149
 
139
- def normalize_markup(markup, needs_apos, needs_expand, needs_amp: false)
150
+ def normalize_markup(markup, needs_apos, needs_expand, needs_amp: false, needs_lt: false)
140
151
  markup = markup.gsub("&apos;", "'") if needs_apos
141
152
  if needs_expand
142
153
  markup = markup.gsub(EMPTY_ELEMENT_RE) do
@@ -144,8 +155,9 @@ module Moxml
144
155
  end
145
156
  end
146
157
  # Segment-aware: this never sees CDATA/comment/PI content,
147
- # where a bare & is literal data.
158
+ # where bare & and < are literal data.
148
159
  markup = markup.gsub(RAW_AMP_RE, "&amp;") if needs_amp
160
+ markup = markup.gsub(RAW_LT_RE, "&lt;") if needs_lt
149
161
  markup
150
162
  end
151
163
  end
@@ -288,6 +288,19 @@ module Moxml
288
288
  return set_attribute_namespace(node, namespace) if node.is_a?(::Leptris::XML::Attr)
289
289
 
290
290
  element = node
291
+ if namespace.nil?
292
+ # Nil-clear contract (issue #164): undeclare the default
293
+ # namespace (xmlns="") and drop any name prefix — the
294
+ # element then reports no namespace, matching the other
295
+ # adapters. Elements that carried a PREFIX cannot fully
296
+ # detach the engine's namespace link (leptris-ruby#132):
297
+ # the name is unqualified and the undeclaration added, but
298
+ # the serializer may re-attach the old prefix until the
299
+ # engine grows a clear entry.
300
+ element.name = element.name.split(":", 2)[-1] if element.name.include?(":")
301
+ element.default_namespace = ""
302
+ return element
303
+ end
291
304
  prefix = namespace.is_a?(String) ? nil : namespace.prefix
292
305
  uri = namespace.is_a?(String) ? namespace : namespace.href
293
306
  if prefix.nil? || prefix.empty?
@@ -170,6 +170,12 @@ module Moxml
170
170
  end
171
171
 
172
172
  def set_namespace(element, ns_or_string)
173
+ if ns_or_string.nil?
174
+ element.namespace_name = nil
175
+ set_attribute(element, "xmlns", "")
176
+ return element
177
+ end
178
+
173
179
  element.namespace_name = ns_or_string.to_s
174
180
  element
175
181
  end
@@ -149,6 +149,12 @@ module Moxml
149
149
  def set_namespace(element, ns)
150
150
  return element unless element.is_a?(::Ox::Element) || element.is_a?(::Ox::Node)
151
151
 
152
+ if ns.nil?
153
+ set_attribute(element, "xmlns", "")
154
+ element.name = element.name.to_s.sub(/\A(?:\w+):/, "")
155
+ return element
156
+ end
157
+
152
158
  prefix = ns.prefix
153
159
  # attributes don't have attributes but can have a namespace prefix
154
160
  if element.is_a?(::Ox::Element)
@@ -485,6 +485,14 @@ module Moxml
485
485
 
486
486
  # add a namespace prefix to the element name AND a namespace definition
487
487
  def set_namespace(element, ns)
488
+ if ns.nil?
489
+ # Nil-clear contract (issue #164): the XML Namespaces 1.0
490
+ # undeclaration, plus an unqualified name.
491
+ element.add_namespace(nil, "") if element.is_a?(::REXML::Element)
492
+ element.name = element.name.to_s.sub(/\A(?:\w+):/, "")
493
+ return element
494
+ end
495
+
488
496
  prefix = ns.name.to_s.empty? ? "xmlns" : ns.name.to_s
489
497
  if element.is_a?(::REXML::Element)
490
498
  element.add_namespace(prefix,
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.26"
4
+ VERSION = "0.5.28"
5
5
  end
@@ -280,6 +280,12 @@ RSpec.shared_examples "Moxml::Namespace" do
280
280
  expect(root.namespace.uri).to eq("http://example.org/1")
281
281
  expect(child.namespace.uri).to eq("http://example.org/2")
282
282
  end
283
+
284
+ it "accepts namespace = nil without raising (issue #164)" do
285
+ cleared = context.parse(%(<r xmlns="urn:clear"><c>t</c></r>))
286
+ child = cleared.root.children.first
287
+ expect { child.namespace = nil }.not_to raise_error
288
+ end
283
289
  end
284
290
  end
285
291
  end
@@ -243,6 +243,55 @@ RSpec.describe Moxml::Adapter::Leptris do
243
243
  expect(doc.root.children.to_a.select(&:text?)).to be_empty
244
244
  end
245
245
 
246
+ it "repairs a raw < in text position (issue #167 / leptris-ruby#131)" do
247
+ # The engine intermittently drops the escape when parsing under
248
+ # allocation pressure: a decoded &#x3c; serializes bare and the
249
+ # output fails to reparse. Valid markup never puts < before a
250
+ # non-name character, so the damage is deterministically
251
+ # repairable in markup segments.
252
+ repaired = described_class.normalize_serialization(
253
+ %(<?xml version="1.0"?><r><pre>A <\n B</pre></r>\n), {}
254
+ )
255
+ expect(repaired).to include("A &lt;")
256
+ expect(repaired).not_to match("A <")
257
+ end
258
+
259
+ it "keeps a literal < inside CDATA untouched by the repair" do
260
+ doc = ctx.parse(%(<r><![CDATA[A < B]]></r>))
261
+ expect(doc.to_xml).to include("<![CDATA[A < B]]>")
262
+ end
263
+
264
+ it "survives parse-under-allocation-pressure without emitting raw < (issue #167)" do
265
+ xml = "<?xml version=\"1.0\"?><doc>#{%(<s><pre alt="A B">A &#x3c;\n B</pre></s>) * 10}</doc>"
266
+ 30.times do
267
+ 100.times { |j| "pressure#{j}" * 20 }
268
+ out = ctx.parse(xml, noblanks: true).to_xml(
269
+ declaration: true, encoding: "UTF-8", indent: 2, expand_empty: false,
270
+ )
271
+ expect(out).not_to match("A <"), "raw unescaped < leaked into text"
272
+ expect(Nokogiri::XML(out, &:strict)).to be_a(Nokogiri::XML::Document)
273
+ end
274
+ end
275
+
276
+ it "clears the namespace with nil — full contract (issue #164)" do
277
+ doc = ctx.parse(%(<r xmlns="urn:clear"><c>t</c></r>))
278
+ child = doc.root.children.first
279
+ child.namespace = nil
280
+ expect(child.namespace).to be_nil
281
+ expect(doc.to_xml).to include(%(<c xmlns="">t</c>))
282
+ reparsed = ctx.parse(doc.to_xml)
283
+ expect(reparsed.root.children.first.namespace).to be_nil
284
+ end
285
+
286
+ it "does not raise clearing a namespace from a prefixed element (issue #164)" do
287
+ # The engine cannot detach a prefix element's namespace link
288
+ # (leptris-ruby#132); the name is unqualified and the
289
+ # undeclaration added meanwhile.
290
+ doc = ctx.parse(%(<r xmlns:p="urn:p"><p:c>t</p:c></r>))
291
+ child = doc.root.children.first
292
+ expect { child.namespace = nil }.not_to raise_error
293
+ end
294
+
246
295
  it "refuses noblanks on readonly parses while the strip path is active" do
247
296
  # The strip mutates the tree; readonly freezes it at parse. On
248
297
  # bindings whose engine flag is libxml2-safe (probe), the flag
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.26
4
+ version: 0.5.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.