moxml 0.5.79 → 0.5.81
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 +4 -4
- data/Gemfile +1 -1
- data/lib/moxml/adapter/base.rb +10 -0
- data/lib/moxml/adapter/leptris/sax_record_replay.rb +60 -0
- data/lib/moxml/adapter/leptris.rb +55 -1
- data/lib/moxml/cdata.rb +5 -0
- data/lib/moxml/comment.rb +5 -0
- data/lib/moxml/element.rb +23 -0
- data/lib/moxml/sax/handler.rb +15 -0
- data/lib/moxml/text.rb +8 -0
- data/lib/moxml/version.rb +1 -1
- data/spec/consistency/eol_normalization_spec.rb +91 -0
- data/spec/consistency/round_trip_spec.rb +16 -8
- data/spec/moxml/adapter/leptris_sax_records_spec.rb +87 -0
- data/spec/moxml/element_add_element_spec.rb +50 -0
- data/spec/moxml/text_spec.rb +17 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 957697bcb855cea040ac0d8b2f202a7cd8fd230482137602fcf7d50fc2712ebb
|
|
4
|
+
data.tar.gz: 22815ac3e9920a8939b0076f3b4222a154e0420d4fe1a21d9b7e56bd9b0818b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 422b076bb191e01b27693af02a66d4e985f574dbcc49451c9af395c1905f9cabfdb2faf0e1b15303a5444c17a8fba98e70e5bea7d5c61f1f152d9fa09a550329
|
|
7
|
+
data.tar.gz: 2d75b66d322414ea8c99b79fe1ed78c7aa758b90b7f5b7f56a685e9f861bca12d6f641803aa04ced575a812677a085619bd40ee435963b035cdbe005a36d1b55
|
data/Gemfile
CHANGED
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -358,6 +358,16 @@ namespace_validation_mode: :strict)
|
|
|
358
358
|
nil
|
|
359
359
|
end
|
|
360
360
|
|
|
361
|
+
# Single-crossing construction contract
|
|
362
|
+
# (Moxml::Element#add_element): create + attach under parent
|
|
363
|
+
# + all attributes in one call, returning the new native
|
|
364
|
+
# element. This default has no bulk face — nil sends the
|
|
365
|
+
# caller down the portable create_element + add_child path;
|
|
366
|
+
# adapters with the face override (leptris — #312/#1344).
|
|
367
|
+
def create_element_with_attrs(_parent, _name, _attrs)
|
|
368
|
+
nil
|
|
369
|
+
end
|
|
370
|
+
|
|
361
371
|
# Read-only attribute listing as [name, value] pairs
|
|
362
372
|
# (Moxml::Element#attribute_pairs) — document order,
|
|
363
373
|
# duplicates included, no Attribute node wrappers. The
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Adapter
|
|
5
|
+
class Leptris
|
|
6
|
+
# Replays the engine's bulk SAX record table (leptris#1298) to
|
|
7
|
+
# the bridge protocol. One drain crossing replaces the
|
|
8
|
+
# per-event recorder replay; the only String materializations
|
|
9
|
+
# left are the name/value/text data the handler actually
|
|
10
|
+
# consumes. End events reconstruct from the records' parent
|
|
11
|
+
# indices with a name-carrying stack — pre-order records make
|
|
12
|
+
# a parent mismatch the exact close boundary, and
|
|
13
|
+
# self-closing elements pop at their next sibling, matching
|
|
14
|
+
# the callback engine's event stream.
|
|
15
|
+
#
|
|
16
|
+
# @private
|
|
17
|
+
class SaxRecordReplay
|
|
18
|
+
KIND_ELEMENT = ::Leptris::XML::SAX::Records::KIND_ELEMENT
|
|
19
|
+
|
|
20
|
+
def initialize(bridge)
|
|
21
|
+
@bridge = bridge
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def run(table)
|
|
25
|
+
@bridge.start_document
|
|
26
|
+
idx_stack = []
|
|
27
|
+
name_stack = []
|
|
28
|
+
count = table.count
|
|
29
|
+
i = 0
|
|
30
|
+
while i < count
|
|
31
|
+
while !idx_stack.empty? && table.parent(i) != idx_stack.last
|
|
32
|
+
idx_stack.pop
|
|
33
|
+
@bridge.end_element(name_stack.pop)
|
|
34
|
+
end
|
|
35
|
+
if table.kind(i) == KIND_ELEMENT
|
|
36
|
+
name = table.view(i)
|
|
37
|
+
pairs = []
|
|
38
|
+
k = table.attr_first(i)
|
|
39
|
+
last = k + table.attr_count_of(i)
|
|
40
|
+
while k < last
|
|
41
|
+
v = table.attr_value(k)
|
|
42
|
+
pairs << [table.attr_name(k),
|
|
43
|
+
table.attr_value_has_ws?(k) ? v.tr("\t\n\r", " ") : v]
|
|
44
|
+
k += 1
|
|
45
|
+
end
|
|
46
|
+
@bridge.start_element(name, pairs)
|
|
47
|
+
idx_stack << i
|
|
48
|
+
name_stack << name
|
|
49
|
+
else
|
|
50
|
+
@bridge.characters(table.view(i))
|
|
51
|
+
end
|
|
52
|
+
i += 1
|
|
53
|
+
end
|
|
54
|
+
@bridge.end_element(name_stack.pop) until name_stack.empty?
|
|
55
|
+
@bridge.end_document
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -130,6 +130,15 @@ module Moxml
|
|
|
130
130
|
defined?(::Leptris::XML::NativeNode) &&
|
|
131
131
|
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.2")
|
|
132
132
|
|
|
133
|
+
# Bulk SAX record drain (leptris#1298): leptris_sax_records_parse
|
|
134
|
+
# + Leptris::XML::SAX::Records — one crossing per document,
|
|
135
|
+
# handlers replay or walk the table. Ships in the binding ride
|
|
136
|
+
# 1.9.240.0 (1.9.239.0 predates the walker — the gate must name
|
|
137
|
+
# the carrying release, not the engine surface). Dev floor pins
|
|
138
|
+
# ~> 1.9.238; older bindings take the callback Parser below.
|
|
139
|
+
NATIVE_SAX_RECORDS =
|
|
140
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.240")
|
|
141
|
+
|
|
133
142
|
# Defined unconditionally: the body self-guards, and call
|
|
134
143
|
# sites must never depend on the layer having loaded
|
|
135
144
|
# (issue #217 — binding-only installs crashed on the
|
|
@@ -522,6 +531,7 @@ module Moxml
|
|
|
522
531
|
autoload :Materialize, "moxml/adapter/leptris/materialize"
|
|
523
532
|
autoload :Serialize, "moxml/adapter/leptris/serialize"
|
|
524
533
|
autoload :LeptrisSAXBridge, "moxml/adapter/leptris/sax_bridge"
|
|
534
|
+
autoload :SaxRecordReplay, "moxml/adapter/leptris/sax_record_replay"
|
|
525
535
|
extend Serialize
|
|
526
536
|
extend DocumentParts
|
|
527
537
|
extend Markers
|
|
@@ -1488,6 +1498,32 @@ module Moxml
|
|
|
1488
1498
|
line.zero? ? nil : line
|
|
1489
1499
|
end
|
|
1490
1500
|
|
|
1501
|
+
# Single-crossing construction (#312/#1344 consumer face):
|
|
1502
|
+
# create + attach under parent + all attributes in ONE
|
|
1503
|
+
# Ruby->C call (create_child is create+attach; the attr
|
|
1504
|
+
# loop is C->C dlsym). Requires parent binding + the face;
|
|
1505
|
+
# callers fall back to create_element + set_attribute +
|
|
1506
|
+
# add_child when absent.
|
|
1507
|
+
def create_element_with_attrs(parent, name, attrs)
|
|
1508
|
+
binding_parent = to_binding(parent)
|
|
1509
|
+
doc = binding_parent.document or return nil
|
|
1510
|
+
return nil unless ::Leptris::XML::Native.respond_to?(:create_element_with_attrs)
|
|
1511
|
+
|
|
1512
|
+
flat = attrs.flatten
|
|
1513
|
+
addr = ::Leptris::XML::Native.create_element_with_attrs(
|
|
1514
|
+
doc.c_address, binding_parent.c_address, name.to_s, flat
|
|
1515
|
+
)
|
|
1516
|
+
return nil if addr.nil? || addr.zero?
|
|
1517
|
+
|
|
1518
|
+
# The binding's own wrap produces the proper binding
|
|
1519
|
+
# Element (kind class, @c_address, @document); canonical
|
|
1520
|
+
# convergence (#219) hands every accessor — children
|
|
1521
|
+
# included — the SAME native, so the wrapper identity
|
|
1522
|
+
# holds across later traversals.
|
|
1523
|
+
canonical_native(doc,
|
|
1524
|
+
::Leptris::XML::Node.wrap(::FFI::Pointer.new(addr), doc))
|
|
1525
|
+
end
|
|
1526
|
+
|
|
1491
1527
|
def attributes(element)
|
|
1492
1528
|
element = to_binding(element)
|
|
1493
1529
|
element.attribute_nodes
|
|
@@ -2089,7 +2125,25 @@ module Moxml
|
|
|
2089
2125
|
def sax_parse(xml, handler)
|
|
2090
2126
|
bridge = LeptrisSAXBridge.new(handler)
|
|
2091
2127
|
xml_string = xml.is_a?(IO) || xml.is_a?(::StringIO) ? xml.read : xml.to_s
|
|
2092
|
-
|
|
2128
|
+
# Bulk record drain (leptris#1298, NATIVE_SAX_RECORDS):
|
|
2129
|
+
# one crossing per document. The handler decides the shape
|
|
2130
|
+
# polymorphically — Handler#on_sax_records answers
|
|
2131
|
+
# :unhandled, so classic handlers get the exact callback
|
|
2132
|
+
# stream replayed from the table; hot loops override and
|
|
2133
|
+
# walk the records (nil table = outside the drain subset
|
|
2134
|
+
# or pre-1.9.238 binding — callback path).
|
|
2135
|
+
table = NATIVE_SAX_RECORDS ? ::Leptris::XML::SAX::Records.open(xml_string) : nil
|
|
2136
|
+
if table
|
|
2137
|
+
begin
|
|
2138
|
+
return if handler.on_sax_records(table) != :unhandled
|
|
2139
|
+
|
|
2140
|
+
SaxRecordReplay.new(bridge).run(table)
|
|
2141
|
+
ensure
|
|
2142
|
+
table.free
|
|
2143
|
+
end
|
|
2144
|
+
else
|
|
2145
|
+
::Leptris::XML::SAX::Parser.new(bridge).parse(xml_string)
|
|
2146
|
+
end
|
|
2093
2147
|
rescue ::Leptris::XML::ParseError, ::Leptris::XML::Error => e
|
|
2094
2148
|
handler.on_error(Moxml::ParseError.new(e.message))
|
|
2095
2149
|
end
|
data/lib/moxml/cdata.rb
CHANGED
data/lib/moxml/comment.rb
CHANGED
data/lib/moxml/element.rb
CHANGED
|
@@ -126,6 +126,29 @@ module Moxml
|
|
|
126
126
|
adapter.attribute_pairs(@native)
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
# Create an element with attributes and attach it under this
|
|
130
|
+
# element — ONE construction on adapters with the bulk face
|
|
131
|
+
# (leptris, #312/#1344 consumer face): create + attach + all
|
|
132
|
+
# attributes in a single Ruby->C crossing. Returns the child
|
|
133
|
+
# wrapper. Equivalent to create_element + attribute writes +
|
|
134
|
+
# add_child, minus two crossings and the intermediate churn.
|
|
135
|
+
def add_element(name, attrs = {})
|
|
136
|
+
native_child = adapter.create_element_with_attrs(
|
|
137
|
+
@native, name, attrs
|
|
138
|
+
)
|
|
139
|
+
if native_child
|
|
140
|
+
adapter.node_type(native_child)
|
|
141
|
+
Moxml::Node.wrap_with(native_child, context, adapter)
|
|
142
|
+
else
|
|
143
|
+
fallback = Moxml::Node.wrap_with(
|
|
144
|
+
adapter.create_element(name), context, adapter
|
|
145
|
+
)
|
|
146
|
+
attrs.each { |k, v| fallback[k] = v.to_s }
|
|
147
|
+
add_child(fallback)
|
|
148
|
+
fallback
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
129
152
|
def attributes
|
|
130
153
|
# Primed like Node.wrap: the adapter and type are known at
|
|
131
154
|
# mint, so the first name/value/attribute? access skips the
|
data/lib/moxml/sax/handler.rb
CHANGED
|
@@ -108,6 +108,21 @@ module Moxml
|
|
|
108
108
|
def on_warning(message)
|
|
109
109
|
# Override in subclass if needed
|
|
110
110
|
end
|
|
111
|
+
|
|
112
|
+
# Bulk SAX record surface (leptris#1298, leptris adapter only).
|
|
113
|
+
# On bindings with the drain the adapter hands the WHOLE
|
|
114
|
+
# document as a Leptris::XML::SAX::Records table — one crossing,
|
|
115
|
+
# no per-event marshaling; hot loops walk the records and
|
|
116
|
+
# materialize only what they consume (this default answers
|
|
117
|
+
# :unhandled, and the adapter replays the classic callback
|
|
118
|
+
# stream from the table instead — the events are identical).
|
|
119
|
+
#
|
|
120
|
+
# @param table [Leptris::XML::SAX::Records] record table, valid
|
|
121
|
+
# only during the call
|
|
122
|
+
# @return [Object] anything but :unhandled claims the table
|
|
123
|
+
def on_sax_records(_table)
|
|
124
|
+
:unhandled
|
|
125
|
+
end
|
|
111
126
|
end
|
|
112
127
|
end
|
|
113
128
|
end
|
data/lib/moxml/text.rb
CHANGED
|
@@ -21,5 +21,13 @@ module Moxml
|
|
|
21
21
|
def to_s
|
|
22
22
|
content
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
# Node#text's base returns "" for non-element nodes; a text
|
|
26
|
+
# node's text IS its content (leptris's Reads layer already
|
|
27
|
+
# answered content here — the wrapper contract now matches on
|
|
28
|
+
# every adapter).
|
|
29
|
+
def text
|
|
30
|
+
content
|
|
31
|
+
end
|
|
24
32
|
end
|
|
25
33
|
end
|
data/lib/moxml/version.rb
CHANGED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# XML 1.0 §2.11 End-of-Line Handling: literal CRLF and lone CR
|
|
6
|
+
# normalize to LF in parsed character data, before tokenization —
|
|
7
|
+
# so CDATA content normalizes too, while character references
|
|
8
|
+
# ( ) sit OUTSIDE the rule and must survive as CR. Attribute
|
|
9
|
+
# values normalize EOL first, then §3.3.3 maps the break to a
|
|
10
|
+
# space (libxml2 parity: "x\r\ny" -> "x y").
|
|
11
|
+
#
|
|
12
|
+
# Adapter status (verified 2026-09-25):
|
|
13
|
+
# nokogiri/libxml - conform on all three surfaces
|
|
14
|
+
# rexml - text/cdata conform; attrs EOL-only ("x\ny")
|
|
15
|
+
# ox - text/cdata conform; attrs raw;
|
|
16
|
+
# over-normalized to \n (upstream)
|
|
17
|
+
# oga - preserves raw CR everywhere (upstream)
|
|
18
|
+
# leptris - fixed in engine 1.9.240 (leptris#1355,
|
|
19
|
+
# leptris-ruby#326); skipped below until the
|
|
20
|
+
# binding release vending that engine rides
|
|
21
|
+
# (gate: binding 1.9.240).
|
|
22
|
+
RSpec.describe "XML line-ending normalization" do
|
|
23
|
+
ADAPTERS = %i[leptris nokogiri ox oga rexml libxml].freeze # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
24
|
+
|
|
25
|
+
def with_adapter(name)
|
|
26
|
+
ctx = Moxml.new(name)
|
|
27
|
+
yield ctx
|
|
28
|
+
rescue LoadError
|
|
29
|
+
skip "adapter #{name} unavailable"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def leptris_fixed?(name)
|
|
33
|
+
return true unless name == :leptris
|
|
34
|
+
|
|
35
|
+
defined?(Leptris::VERSION) &&
|
|
36
|
+
Gem::Version.new(Leptris::VERSION) >= Gem::Version.new("1.9.240")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "normalizes CRLF and lone CR to LF in text content" do
|
|
40
|
+
ADAPTERS.each do |name|
|
|
41
|
+
next unless leptris_fixed?(name)
|
|
42
|
+
|
|
43
|
+
skip "oga preserves raw CR (upstream)" if name == :oga
|
|
44
|
+
|
|
45
|
+
with_adapter(name) do |ctx|
|
|
46
|
+
text = ctx.parse("<r>a\r\nb\rc</r>").root.children.first
|
|
47
|
+
expect(text.content).to eq("a\nb\nc"), "#{name}: #{text.content.inspect}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "normalizes EOL inside CDATA content" do
|
|
53
|
+
ADAPTERS.each do |name|
|
|
54
|
+
next unless leptris_fixed?(name)
|
|
55
|
+
|
|
56
|
+
skip "oga preserves raw CR (upstream)" if name == :oga
|
|
57
|
+
|
|
58
|
+
with_adapter(name) do |ctx|
|
|
59
|
+
cdata = ctx.parse("<r><![CDATA[a\r\nb]]></r>").root.children.first
|
|
60
|
+
expect(cdata.content).to eq("a\nb"), "#{name}: #{cdata.content.inspect}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "preserves CR from character references" do
|
|
66
|
+
ADAPTERS.each do |name|
|
|
67
|
+
next unless leptris_fixed?(name)
|
|
68
|
+
|
|
69
|
+
skip "ox collapses to LF (upstream)" if name == :ox
|
|
70
|
+
skip "oga preserves raw CR (upstream)" if name == :oga
|
|
71
|
+
|
|
72
|
+
with_adapter(name) do |ctx|
|
|
73
|
+
text = ctx.parse("<r>a b</r>").root.children.first
|
|
74
|
+
expect(text.content).to eq("a\rb"), "#{name}: #{text.content.inspect}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "collapses attribute CRLF to a single space (libxml2 parity)" do
|
|
80
|
+
conforming = %i[nokogiri libxml leptris]
|
|
81
|
+
conforming.each do |name|
|
|
82
|
+
next unless leptris_fixed?(name)
|
|
83
|
+
|
|
84
|
+
with_adapter(name) do |ctx|
|
|
85
|
+
root = ctx.parse("<r a=\"x\r\ny\" b=\"x\ty\"/>").root
|
|
86
|
+
expect(root["a"]).to eq("x y"), "#{name}: attr CRLF #{root['a'].inspect}"
|
|
87
|
+
expect(root["b"]).to eq("x y"), "#{name}: attr tab #{root['b'].inspect}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -295,16 +295,24 @@ def normalize_type_attribute(name, value)
|
|
|
295
295
|
end
|
|
296
296
|
end
|
|
297
297
|
|
|
298
|
-
def test_element_content(
|
|
299
|
-
return nil unless
|
|
298
|
+
def test_element_content(node)
|
|
299
|
+
return nil unless node
|
|
300
|
+
|
|
301
|
+
# :text_content holds a Text node (from //text()) — Text#text now
|
|
302
|
+
# returns real content on every adapter, so this branch actually
|
|
303
|
+
# fires. Text nodes have no name/attributes/children.
|
|
304
|
+
unless node.is_a?(Moxml::Element)
|
|
305
|
+
return { name: nil, attributes: {}, text: node.text.to_s.strip,
|
|
306
|
+
namespace: nil, children_count: 0, xpath: [] }
|
|
307
|
+
end
|
|
300
308
|
|
|
301
309
|
{
|
|
302
|
-
name:
|
|
303
|
-
attributes: universal_attributes(
|
|
304
|
-
text:
|
|
305
|
-
namespace:
|
|
306
|
-
children_count:
|
|
307
|
-
xpath:
|
|
310
|
+
name: node.name,
|
|
311
|
+
attributes: universal_attributes(node),
|
|
312
|
+
text: node.text.to_s.strip,
|
|
313
|
+
namespace: node.namespace&.uri,
|
|
314
|
+
children_count: node.children.size,
|
|
315
|
+
xpath: node.xpath("//*"),
|
|
308
316
|
}
|
|
309
317
|
end
|
|
310
318
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Bulk SAX record drain (leptris#1298): on bindings carrying
|
|
6
|
+
# Leptris::XML::SAX::Records the leptris adapter drains the document
|
|
7
|
+
# in one crossing. Classic handlers must receive the SAME event
|
|
8
|
+
# stream the callback Recorder produces (the replay reconstructs end
|
|
9
|
+
# events from the records' parent indices), handlers overriding
|
|
10
|
+
# on_sax_records claim the raw table instead, and raw attribute
|
|
11
|
+
# whitespace is normalized (3.3.3) exactly where the callback path
|
|
12
|
+
# normalizes it.
|
|
13
|
+
module SaxDrainHandlers
|
|
14
|
+
class EventCollector < Moxml::SAX::Handler
|
|
15
|
+
attr_reader :events
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
super
|
|
19
|
+
@events = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def on_start_document
|
|
23
|
+
@events << [:start_document]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def on_end_document
|
|
27
|
+
@events << [:end_document]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def on_start_element(name, attributes = {}, _namespaces = {})
|
|
31
|
+
@events << [:start, name, attributes]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def on_end_element(name)
|
|
35
|
+
@events << [:end, name]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def on_characters(text)
|
|
39
|
+
@events << [:chars, text]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class TableClaimant < Moxml::SAX::Handler
|
|
44
|
+
attr_reader :tables
|
|
45
|
+
|
|
46
|
+
def initialize
|
|
47
|
+
super
|
|
48
|
+
@tables = []
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def on_sax_records(table)
|
|
52
|
+
@tables << table
|
|
53
|
+
:claimed
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
RSpec.describe "leptris SAX record drain", if: defined?(Leptris::XML::SAX::Records) do
|
|
59
|
+
let(:context) { Moxml.new(:leptris) }
|
|
60
|
+
let(:xml) do
|
|
61
|
+
'<r a="x y" xmlns="urn:default"><item n="1">t1</item>' \
|
|
62
|
+
'<item n="2">t2<b>k</b></item><last/></r>'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "replays the callback event stream through the drain" do
|
|
66
|
+
drain_events = SaxDrainHandlers::EventCollector.new
|
|
67
|
+
context.sax_parse(xml, drain_events)
|
|
68
|
+
|
|
69
|
+
callback_events = SaxDrainHandlers::EventCollector.new
|
|
70
|
+
bridge = Moxml::Adapter::Leptris::LeptrisSAXBridge.new(callback_events)
|
|
71
|
+
Leptris::XML::SAX::Parser.new(bridge).parse(xml)
|
|
72
|
+
|
|
73
|
+
expect(drain_events.events).to eq(callback_events.events)
|
|
74
|
+
expect(drain_events.events.first).to eq([:start_document])
|
|
75
|
+
expect(drain_events.events).to include([:start, "item", { "n" => "1" }])
|
|
76
|
+
expect(drain_events.events).to include([:chars, "t2"])
|
|
77
|
+
expect(drain_events.events.last).to eq([:end_document])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "hands claimed tables to handlers that opt in" do
|
|
81
|
+
claimant = SaxDrainHandlers::TableClaimant.new
|
|
82
|
+
context.sax_parse("<r><a>n</a></r>", claimant)
|
|
83
|
+
|
|
84
|
+
expect(claimant.tables.size).to eq(1)
|
|
85
|
+
expect(claimant.tables.first.count).to eq(3)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Element#add_element adapter-contract conformance (#312): the
|
|
6
|
+
# child must land attached with ALL attributes in one call —
|
|
7
|
+
# through the bulk face (leptris) or the portable create_element +
|
|
8
|
+
# add_child fallback (every other adapter). Guards both halves of
|
|
9
|
+
# the contract: a missing base default NoMethodErrors on non-face
|
|
10
|
+
# adapters, a broken fallback loses attributes or the attach.
|
|
11
|
+
RSpec.describe Moxml::Element do
|
|
12
|
+
ADAPTERS = %i[leptris nokogiri ox oga rexml libxml].freeze # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
13
|
+
|
|
14
|
+
def with_adapter(name)
|
|
15
|
+
ctx = Moxml.new(name)
|
|
16
|
+
yield ctx
|
|
17
|
+
rescue StandardError, LoadError
|
|
18
|
+
skip "adapter #{name} unavailable"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "attaches a child with all attributes on every available adapter" do
|
|
22
|
+
ADAPTERS.each do |name|
|
|
23
|
+
with_adapter(name) do |ctx|
|
|
24
|
+
doc = ctx.parse("<catalog/>")
|
|
25
|
+
root = doc.root
|
|
26
|
+
child = root.add_element("record", "id" => "r1", "kind" => "k1")
|
|
27
|
+
|
|
28
|
+
expect(child.name).to eq("record"), "#{name}: name"
|
|
29
|
+
expect(child["id"]).to eq("r1"), "#{name}: id attr"
|
|
30
|
+
expect(child["kind"]).to eq("k1"), "#{name}: kind attr"
|
|
31
|
+
expect(child.parent.name).to eq("catalog"), "#{name}: attach"
|
|
32
|
+
expect(root.children.map(&:name)).to include("record"), "#{name}: visible in children"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "nests under a non-root element with empty attrs" do
|
|
38
|
+
ADAPTERS.each do |name|
|
|
39
|
+
with_adapter(name) do |ctx|
|
|
40
|
+
doc = ctx.parse("<catalog><record/></catalog>")
|
|
41
|
+
record = doc.root.children.first
|
|
42
|
+
field = record.add_element("field")
|
|
43
|
+
|
|
44
|
+
expect(field.name).to eq("field"), "#{name}: name"
|
|
45
|
+
expect(field.attributes).to be_empty, "#{name}: no attrs"
|
|
46
|
+
expect(field.parent.name).to eq("record"), "#{name}: attach"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/spec/moxml/text_spec.rb
CHANGED
|
@@ -14,6 +14,23 @@ RSpec.describe Moxml::Text do
|
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
describe "#text" do
|
|
18
|
+
it "matches content on a text node" do
|
|
19
|
+
text = doc.root.children.first
|
|
20
|
+
expect(text.text).to eq("plain text")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "is consistent across adapters" do
|
|
24
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
25
|
+
ctx = Moxml.new(adapter_name)
|
|
26
|
+
d = ctx.parse("<root>hello world</root>")
|
|
27
|
+
text = d.root.children.first
|
|
28
|
+
expect(text.text).to eq("hello world"),
|
|
29
|
+
"Text#text for #{adapter_name} adapter"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
17
34
|
describe "#to_xml" do
|
|
18
35
|
it "returns XML representation" do
|
|
19
36
|
text = doc.root.children.first
|
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.
|
|
4
|
+
version: 0.5.81
|
|
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-
|
|
11
|
+
date: 2026-09-25 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
|
|
@@ -160,6 +160,7 @@ files:
|
|
|
160
160
|
- lib/moxml/adapter/leptris/markers.rb
|
|
161
161
|
- lib/moxml/adapter/leptris/materialize.rb
|
|
162
162
|
- lib/moxml/adapter/leptris/sax_bridge.rb
|
|
163
|
+
- lib/moxml/adapter/leptris/sax_record_replay.rb
|
|
163
164
|
- lib/moxml/adapter/leptris/serialize.rb
|
|
164
165
|
- lib/moxml/adapter/libxml.rb
|
|
165
166
|
- lib/moxml/adapter/libxml/entity_ref_registry.rb
|
|
@@ -300,6 +301,7 @@ files:
|
|
|
300
301
|
- sig/moxml.rbs
|
|
301
302
|
- spec/consistency/README.md
|
|
302
303
|
- spec/consistency/adapter_parity_spec.rb
|
|
304
|
+
- spec/consistency/eol_normalization_spec.rb
|
|
303
305
|
- spec/consistency/round_trip_spec.rb
|
|
304
306
|
- spec/examples/README.md
|
|
305
307
|
- spec/examples/attribute_examples_spec.rb
|
|
@@ -435,6 +437,7 @@ files:
|
|
|
435
437
|
- spec/moxml/adapter/entity_restoration_spec.rb
|
|
436
438
|
- spec/moxml/adapter/headed_ox_spec.rb
|
|
437
439
|
- spec/moxml/adapter/leptris_doc_parts_spec.rb
|
|
440
|
+
- spec/moxml/adapter/leptris_sax_records_spec.rb
|
|
438
441
|
- spec/moxml/adapter/leptris_spec.rb
|
|
439
442
|
- spec/moxml/adapter/libxml_internals_spec.rb
|
|
440
443
|
- spec/moxml/adapter/libxml_spec.rb
|
|
@@ -468,6 +471,7 @@ files:
|
|
|
468
471
|
- spec/moxml/document_free_spec.rb
|
|
469
472
|
- spec/moxml/document_parse_diagnostics_spec.rb
|
|
470
473
|
- spec/moxml/document_spec.rb
|
|
474
|
+
- spec/moxml/element_add_element_spec.rb
|
|
471
475
|
- spec/moxml/element_attribute_pairs_spec.rb
|
|
472
476
|
- spec/moxml/element_spec.rb
|
|
473
477
|
- spec/moxml/entity_preservation_spec.rb
|