moxml 0.1.17 → 0.1.19
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/README.adoc +24 -1
- data/lib/moxml/adapter/libxml.rb +0 -3
- data/lib/moxml/adapter/nokogiri.rb +1 -5
- data/lib/moxml/adapter/oga.rb +11 -5
- data/lib/moxml/adapter/rexml.rb +2 -6
- data/lib/moxml/config.rb +17 -2
- data/lib/moxml/native_attachment/native.rb +69 -0
- data/lib/moxml/native_attachment/opal.rb +34 -0
- data/lib/moxml/native_attachment.rb +16 -46
- data/lib/moxml/version.rb +1 -1
- data/spec/integration/shared_examples/edge_cases.rb +85 -0
- data/spec/integration/shared_examples/entity_reference_whitespace.rb +5 -3
- data/spec/integration/shared_examples/high_level/document_builder_behavior.rb +8 -6
- data/spec/integration/shared_examples/integration_workflows.rb +1 -1
- data/spec/integration/shared_examples/node_wrappers/node_set_behavior.rb +3 -1
- data/spec/moxml/lazy_parse_spec.rb +1 -1
- data/spec/moxml/moxml_spec.rb +6 -1
- data/spec/moxml/native_attachment/native_spec.rb +57 -0
- data/spec/moxml/native_attachment/opal_spec.rb +29 -0
- data/spec/moxml/native_attachment/shared_examples.rb +43 -0
- data/spec/moxml/native_attachment_spec.rb +184 -0
- data/spec/moxml/xpath/functions/node_functions_spec.rb +3 -2
- data/spec/performance/benchmark_spec.rb +2 -2
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 76508cc0d2699469ade87f48e38ea73289abc4f95cb7a313f6d71548477248f3
|
|
4
|
+
data.tar.gz: 1752cb433953a869cedd1d88fc565c53f77bd08201c5977247150e1e7b75a386
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a4219577f2e00a9e4f00f1ce1bbaf4f03d6c841dfe8adb9983162a6389b1e276725984dc010f451cac55fbd8049a81451d641fc073fc1cf8417f90ee6e50cdf5
|
|
7
|
+
data.tar.gz: 44f3e08b174e2689fed3a60ee3ef0b805fdc14cbeeb905ebde15e5241be3e3f168441dc95afe99dd244fc1047c581fce912ec3e061299e4e101f63329f00def9
|
data/README.adoc
CHANGED
|
@@ -33,7 +33,7 @@ Moxml supports the following XML libraries:
|
|
|
33
33
|
REXML:: https://github.com/ruby/rexml[REXML], a pure Ruby XML parser
|
|
34
34
|
distributed with standard Ruby. Not the fastest, but always available.
|
|
35
35
|
|
|
36
|
-
Nokogiri::
|
|
36
|
+
Nokogiri:: https://github.com/sparklemotion/nokogiri[Nokogiri], a
|
|
37
37
|
widely used implementation which wraps around the performant
|
|
38
38
|
https://github.com/GNOME/libxml2[libxml2] C library.
|
|
39
39
|
|
|
@@ -47,6 +47,29 @@ LibXML:: https://github.com/xml4r/libxml-ruby[libxml-ruby], Ruby bindings
|
|
|
47
47
|
for the performant https://github.com/GNOME/libxml2[libxml2] C library.
|
|
48
48
|
Alternative to Nokogiri with similar performance characteristics.
|
|
49
49
|
|
|
50
|
+
==== Default adapter selection
|
|
51
|
+
|
|
52
|
+
When no adapter is explicitly specified, Moxml selects one automatically based on
|
|
53
|
+
the runtime environment:
|
|
54
|
+
|
|
55
|
+
. If running on Opal (`RUBY_ENGINE == "opal"`), Oga is used (pure Ruby, no C extensions)
|
|
56
|
+
. Otherwise, Moxml detects already-loaded XML libraries in this order:
|
|
57
|
+
.. Nokogiri
|
|
58
|
+
.. Ox
|
|
59
|
+
.. Oga
|
|
60
|
+
. If none of the above are loaded, Nokogiri is the fallback default
|
|
61
|
+
|
|
62
|
+
[source,ruby]
|
|
63
|
+
----
|
|
64
|
+
# Automatic detection — picks the best available adapter
|
|
65
|
+
ctx = Moxml.new
|
|
66
|
+
ctx.config.adapter_name # => :nokogiri (or :oga on Opal)
|
|
67
|
+
|
|
68
|
+
# Explicit override — always takes precedence
|
|
69
|
+
ctx = Moxml.new(:ox)
|
|
70
|
+
ctx.config.adapter_name # => :ox
|
|
71
|
+
----
|
|
72
|
+
|
|
50
73
|
=== Feature table
|
|
51
74
|
|
|
52
75
|
Moxml exercises its best effort to provide a consistent interface across basic
|
data/lib/moxml/adapter/libxml.rb
CHANGED
|
@@ -182,11 +182,7 @@ module Moxml
|
|
|
182
182
|
end
|
|
183
183
|
|
|
184
184
|
def children(node)
|
|
185
|
-
node.children
|
|
186
|
-
child.text? && child.content.strip.empty? &&
|
|
187
|
-
!(child.previous_sibling.nil? && child.next_sibling.nil?) &&
|
|
188
|
-
!adjacent_to_entity_reference?(child)
|
|
189
|
-
end
|
|
185
|
+
node.children
|
|
190
186
|
end
|
|
191
187
|
|
|
192
188
|
def adjacent_to_entity_reference?(node)
|
data/lib/moxml/adapter/oga.rb
CHANGED
|
@@ -193,12 +193,18 @@ module Moxml
|
|
|
193
193
|
|
|
194
194
|
return all_children unless node.is_a?(::Oga::XML::Node) || node.is_a?(::Oga::XML::Document)
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
196
|
+
child_nodes = node.children.to_a
|
|
197
|
+
# Filter out whitespace-only text nodes at document level only.
|
|
198
|
+
# Document-level whitespace (between <?xml?> and <root>) is
|
|
199
|
+
# formatting, not content, and differs across adapters.
|
|
200
|
+
# Whitespace inside elements (e.g. "FigureA.1" spacing) is
|
|
201
|
+
# meaningful and must be preserved.
|
|
202
|
+
if node.is_a?(::Oga::XML::Document)
|
|
203
|
+
child_nodes = child_nodes.reject do |child|
|
|
204
|
+
child.is_a?(::Oga::XML::Text) && child.text.strip.empty?
|
|
205
|
+
end
|
|
201
206
|
end
|
|
207
|
+
all_children + child_nodes
|
|
202
208
|
end
|
|
203
209
|
|
|
204
210
|
def adjacent_to_entity_reference?(node)
|
data/lib/moxml/adapter/rexml.rb
CHANGED
|
@@ -177,12 +177,8 @@ module Moxml
|
|
|
177
177
|
def children(node)
|
|
178
178
|
return [] unless node.is_a?(::REXML::Parent)
|
|
179
179
|
|
|
180
|
-
#
|
|
181
|
-
result = node.children.
|
|
182
|
-
child.is_a?(::REXML::Text) &&
|
|
183
|
-
child.to_s.strip.empty? &&
|
|
184
|
-
!(child.next_sibling.nil? && child.previous_sibling.nil?)
|
|
185
|
-
end
|
|
180
|
+
# Return all children preserving whitespace text nodes
|
|
181
|
+
result = node.children.dup
|
|
186
182
|
|
|
187
183
|
# Include any EntityReference wrappers stored alongside native children
|
|
188
184
|
entity_refs = attachments.get(node, :entity_refs)
|
data/lib/moxml/config.rb
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
module Moxml
|
|
4
4
|
class Config
|
|
5
5
|
VALID_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml].freeze
|
|
6
|
-
DEFAULT_ADAPTER =
|
|
6
|
+
DEFAULT_ADAPTER = :nokogiri
|
|
7
|
+
OPAL_DEFAULT_ADAPTER = :oga
|
|
7
8
|
|
|
8
9
|
# Entity loading modes:
|
|
9
10
|
# - :required - Must load entities, raise error if unavailable (default)
|
|
@@ -20,7 +21,21 @@ module Moxml
|
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def default_adapter
|
|
23
|
-
@default_adapter
|
|
24
|
+
@default_adapter || runtime_default_adapter
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def runtime_default_adapter
|
|
28
|
+
return OPAL_DEFAULT_ADAPTER if RUBY_ENGINE == "opal"
|
|
29
|
+
|
|
30
|
+
detect_loaded_adapter || DEFAULT_ADAPTER
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def detect_loaded_adapter
|
|
34
|
+
return :nokogiri if Object.const_defined?(:Nokogiri)
|
|
35
|
+
return :ox if Object.const_defined?(:Ox)
|
|
36
|
+
return :oga if Object.const_defined?(:Oga)
|
|
37
|
+
|
|
38
|
+
nil
|
|
24
39
|
end
|
|
25
40
|
end
|
|
26
41
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module Moxml
|
|
6
|
+
class NativeAttachment
|
|
7
|
+
# Stores Moxml-specific state associated with native adapter objects
|
|
8
|
+
# without polluting their internals.
|
|
9
|
+
#
|
|
10
|
+
# Uses object_id as key with GC finalizer cleanup to prevent memory leaks.
|
|
11
|
+
# Thread-safe via Monitor (reentrant-safe).
|
|
12
|
+
#
|
|
13
|
+
# Replaces the anti-pattern of using instance_variable_set/get on
|
|
14
|
+
# foreign library objects (Nokogiri, REXML, Oga, Ox, LibXML nodes).
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# attachments = NativeAttachment.new
|
|
18
|
+
# attachments.set(native_element, :entity_refs, [])
|
|
19
|
+
# refs = attachments.get(native_element, :entity_refs)
|
|
20
|
+
# attachments.key?(native_element, :doctype) #=> false
|
|
21
|
+
class Native
|
|
22
|
+
def initialize
|
|
23
|
+
@data = {}
|
|
24
|
+
@finalizer_registered = {}
|
|
25
|
+
@monitor = Monitor.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def get(native, key)
|
|
29
|
+
@monitor.synchronize { @data[native.object_id]&.[](key) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def set(native, key, value)
|
|
33
|
+
id = native.object_id
|
|
34
|
+
@monitor.synchronize do
|
|
35
|
+
@data[id] ||= {}
|
|
36
|
+
@data[id][key] = value
|
|
37
|
+
register_finalizer(native, id) unless @finalizer_registered[id]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def key?(native, key)
|
|
42
|
+
@monitor.synchronize { @data[native.object_id]&.key?(key) || false }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def delete(native, key)
|
|
46
|
+
@monitor.synchronize { @data[native.object_id]&.delete(key) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def register_finalizer(native, id)
|
|
52
|
+
@finalizer_registered[id] = true
|
|
53
|
+
ObjectSpace.define_finalizer(native, finalizer_for(id))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def finalizer_for(id)
|
|
57
|
+
data = @data
|
|
58
|
+
registered = @finalizer_registered
|
|
59
|
+
# Finalizers must NOT use Mutex/Monitor (can't be called from trap context).
|
|
60
|
+
# Direct Hash operations are safe here since finalizers run sequentially
|
|
61
|
+
# and the GC'd object's id won't be accessed by any other thread.
|
|
62
|
+
proc do
|
|
63
|
+
data.delete(id)
|
|
64
|
+
registered.delete(id)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
class NativeAttachment
|
|
5
|
+
# Opal adapter nodes are Ruby objects, so instance variables are sufficient
|
|
6
|
+
# for Moxml-owned attachments without relying on Monitor/Thread support.
|
|
7
|
+
class Opal
|
|
8
|
+
def get(native, key)
|
|
9
|
+
native.instance_variable_get(attachment_ivar_name(key))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def set(native, key, value)
|
|
13
|
+
native.instance_variable_set(attachment_ivar_name(key), value)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def key?(native, key)
|
|
17
|
+
native.instance_variable_defined?(attachment_ivar_name(key))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def delete(native, key)
|
|
21
|
+
ivar_name = attachment_ivar_name(key)
|
|
22
|
+
return unless native.instance_variable_defined?(ivar_name)
|
|
23
|
+
|
|
24
|
+
native.remove_instance_variable(ivar_name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def attachment_ivar_name(key)
|
|
30
|
+
:"@moxml_attachment_#{key}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -1,65 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Moxml
|
|
4
|
-
# Stores Moxml-specific state associated with native adapter objects
|
|
5
|
-
# without polluting their internals.
|
|
6
|
-
#
|
|
7
|
-
# Uses object_id as key with GC finalizer cleanup to prevent memory leaks.
|
|
8
|
-
# Thread-safe via Monitor (reentrant-safe).
|
|
9
|
-
#
|
|
10
|
-
# Replaces the anti-pattern of using instance_variable_set/get on
|
|
11
|
-
# foreign library objects (Nokogiri, REXML, Oga, Ox, LibXML nodes).
|
|
12
|
-
#
|
|
13
|
-
# @example
|
|
14
|
-
# attachments = NativeAttachment.new
|
|
15
|
-
# attachments.set(native_element, :entity_refs, [])
|
|
16
|
-
# refs = attachments.get(native_element, :entity_refs)
|
|
17
|
-
# attachments.key?(native_element, :doctype) #=> false
|
|
18
4
|
class NativeAttachment
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
autoload :Opal, "moxml/native_attachment/opal"
|
|
6
|
+
autoload :Native, "moxml/native_attachment/native"
|
|
7
|
+
|
|
8
|
+
def self.default_backend
|
|
9
|
+
constant = RUBY_ENGINE == "opal" ? :Opal : :Native
|
|
10
|
+
const_get(constant).new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :backend
|
|
14
|
+
|
|
15
|
+
def initialize(backend: self.class.default_backend)
|
|
16
|
+
@backend = backend
|
|
23
17
|
end
|
|
24
18
|
|
|
25
19
|
def get(native, key)
|
|
26
|
-
@
|
|
20
|
+
@backend.get(native, key)
|
|
27
21
|
end
|
|
28
22
|
|
|
29
23
|
def set(native, key, value)
|
|
30
|
-
|
|
31
|
-
@monitor.synchronize do
|
|
32
|
-
@data[id] ||= {}
|
|
33
|
-
@data[id][key] = value
|
|
34
|
-
register_finalizer(native, id) unless @finalizer_registered[id]
|
|
35
|
-
end
|
|
24
|
+
@backend.set(native, key, value)
|
|
36
25
|
end
|
|
37
26
|
|
|
38
27
|
def key?(native, key)
|
|
39
|
-
@
|
|
28
|
+
@backend.key?(native, key)
|
|
40
29
|
end
|
|
41
30
|
|
|
42
31
|
def delete(native, key)
|
|
43
|
-
@
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
def register_finalizer(native, id)
|
|
49
|
-
@finalizer_registered[id] = true
|
|
50
|
-
ObjectSpace.define_finalizer(native, finalizer_for(id))
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def finalizer_for(id)
|
|
54
|
-
data = @data
|
|
55
|
-
registered = @finalizer_registered
|
|
56
|
-
# Finalizers must NOT use Mutex/Monitor (can't be called from trap context).
|
|
57
|
-
# Direct Hash operations are safe here since finalizers run sequentially
|
|
58
|
-
# and the GC'd object's id won't be accessed by any other thread.
|
|
59
|
-
proc do
|
|
60
|
-
data.delete(id)
|
|
61
|
-
registered.delete(id)
|
|
62
|
-
end
|
|
32
|
+
@backend.delete(native, key)
|
|
63
33
|
end
|
|
64
34
|
end
|
|
65
35
|
end
|
data/lib/moxml/version.rb
CHANGED
|
@@ -167,6 +167,91 @@ RSpec.shared_examples "Moxml Edge Cases" do
|
|
|
167
167
|
end
|
|
168
168
|
end
|
|
169
169
|
|
|
170
|
+
describe "whitespace text node preservation" do
|
|
171
|
+
# Ox/HeadedOx do not generate whitespace-only text nodes in their parser,
|
|
172
|
+
# so these tests only apply to adapters that do (Nokogiri, OGA, REXML, LibXML)
|
|
173
|
+
let(:preserves_ws) { !%i[ox headed_ox].include?(context.config.adapter_name) }
|
|
174
|
+
|
|
175
|
+
it "preserves whitespace-only text nodes between sibling elements" do
|
|
176
|
+
unless preserves_ws
|
|
177
|
+
skip "Ox/HeadedOx parser does not generate whitespace-only text nodes"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
xml = <<~XML
|
|
181
|
+
<root>
|
|
182
|
+
<a>1</a>
|
|
183
|
+
<b>2</b>
|
|
184
|
+
<c>3</c>
|
|
185
|
+
</root>
|
|
186
|
+
XML
|
|
187
|
+
|
|
188
|
+
doc = context.parse(xml)
|
|
189
|
+
children = doc.root.children
|
|
190
|
+
|
|
191
|
+
# Should have whitespace text nodes between elements
|
|
192
|
+
expect(children.size).to be > 3
|
|
193
|
+
|
|
194
|
+
# Whitespace text nodes should be Text nodes
|
|
195
|
+
ws_nodes = children.select { |c| c.is_a?(Moxml::Text) && c.content.strip.empty? }
|
|
196
|
+
expect(ws_nodes).not_to be_empty
|
|
197
|
+
|
|
198
|
+
# Element children should still be accessible
|
|
199
|
+
elements = children.select { |c| c.is_a?(Moxml::Element) }
|
|
200
|
+
expect(elements.map(&:name)).to eq(%w[a b c])
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it "preserves inline whitespace text nodes between text and elements" do
|
|
204
|
+
xml = "<p>Figure <sub>A</sub>.1</p>"
|
|
205
|
+
doc = context.parse(xml)
|
|
206
|
+
|
|
207
|
+
children = doc.root.children
|
|
208
|
+
expect(children.size).to eq(3)
|
|
209
|
+
|
|
210
|
+
# First child: "Figure " text node
|
|
211
|
+
expect(children[0]).to be_a(Moxml::Text)
|
|
212
|
+
expect(children[0].content).to eq("Figure ")
|
|
213
|
+
|
|
214
|
+
# Second child: <sub> element
|
|
215
|
+
expect(children[1]).to be_a(Moxml::Element)
|
|
216
|
+
expect(children[1].name).to eq("sub")
|
|
217
|
+
expect(children[1].text).to eq("A")
|
|
218
|
+
|
|
219
|
+
# Third child: ".1" text node
|
|
220
|
+
expect(children[2]).to be_a(Moxml::Text)
|
|
221
|
+
expect(children[2].content).to eq(".1")
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it "preserves space-only text node as meaningful content" do
|
|
225
|
+
xml = "<p>Hello <b>world</b>!</p>"
|
|
226
|
+
doc = context.parse(xml)
|
|
227
|
+
|
|
228
|
+
children = doc.root.children
|
|
229
|
+
expect(children.size).to eq(3)
|
|
230
|
+
|
|
231
|
+
expect(children[0].content).to eq("Hello ")
|
|
232
|
+
expect(children[1]).to be_a(Moxml::Element)
|
|
233
|
+
expect(children[2].content).to eq("!")
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it "distinguishes whitespace text nodes from element children" do
|
|
237
|
+
unless preserves_ws
|
|
238
|
+
skip "Ox/HeadedOx parser does not generate whitespace-only text nodes"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
xml = "<root> <child/> </root>"
|
|
242
|
+
doc = context.parse(xml)
|
|
243
|
+
|
|
244
|
+
children = doc.root.children
|
|
245
|
+
# " " before child, " " after child
|
|
246
|
+
expect(children.size).to eq(3)
|
|
247
|
+
expect(children[0]).to be_a(Moxml::Text)
|
|
248
|
+
expect(children[0].content).to eq(" ")
|
|
249
|
+
expect(children[1]).to be_a(Moxml::Element)
|
|
250
|
+
expect(children[2]).to be_a(Moxml::Text)
|
|
251
|
+
expect(children[2].content).to eq(" ")
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
170
255
|
describe "document structure edge cases" do
|
|
171
256
|
it "handles deeply nested elements" do
|
|
172
257
|
doc = context.create_document
|
|
@@ -104,7 +104,7 @@ RSpec.shared_examples "Entity Reference Whitespace Preservation" do
|
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
describe "structural whitespace filtering" do
|
|
107
|
-
it "
|
|
107
|
+
it "preserves whitespace text nodes between elements" do
|
|
108
108
|
xml = <<~XML
|
|
109
109
|
<root>
|
|
110
110
|
<child1/>
|
|
@@ -115,8 +115,10 @@ RSpec.shared_examples "Entity Reference Whitespace Preservation" do
|
|
|
115
115
|
doc = context.parse(xml)
|
|
116
116
|
children = doc.root.children
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
# Whitespace text nodes between elements are preserved
|
|
119
|
+
elements = children.select { |c| c.is_a?(Moxml::Element) }
|
|
120
|
+
expect(elements.length).to eq(2)
|
|
121
|
+
expect(elements.map(&:name)).to eq(%w[child1 child2])
|
|
120
122
|
end
|
|
121
123
|
end
|
|
122
124
|
end
|
|
@@ -32,12 +32,14 @@ RSpec.shared_examples "Moxml::DocumentBuilder" do
|
|
|
32
32
|
|
|
33
33
|
expect(doc.root.namespaces.count).to eq(1)
|
|
34
34
|
expect(doc.root.namespaces.first.uri).to eq("http://example.org")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
expect(
|
|
38
|
-
expect(
|
|
39
|
-
expect(
|
|
40
|
-
expect(
|
|
35
|
+
# Whitespace text nodes are preserved between elements
|
|
36
|
+
non_ws_children = doc.root.children.reject { |c| c.is_a?(Moxml::Text) && c.content.strip.empty? }
|
|
37
|
+
expect(non_ws_children[0]).to be_a(Moxml::Comment)
|
|
38
|
+
expect(non_ws_children[1]).to be_a(Moxml::Element)
|
|
39
|
+
expect(non_ws_children[1].name).to eq("child")
|
|
40
|
+
expect(non_ws_children[1]["id"]).to eq("1")
|
|
41
|
+
expect(non_ws_children[1].children.find { |c| c.is_a?(Moxml::Cdata) }).to be_a(Moxml::Cdata)
|
|
42
|
+
expect(non_ws_children[2]).to be_a(Moxml::ProcessingInstruction)
|
|
41
43
|
end
|
|
42
44
|
end
|
|
43
45
|
end
|
|
@@ -113,7 +113,7 @@ RSpec.shared_examples "Moxml Integration" do
|
|
|
113
113
|
expect(attr).to eq("value")
|
|
114
114
|
|
|
115
115
|
# Test namespace override
|
|
116
|
-
deeper = a_element.children.
|
|
116
|
+
deeper = a_element.children.find { |c| c.is_a?(Moxml::Element) }
|
|
117
117
|
expect(deeper.namespace.uri).to eq("http://other.org")
|
|
118
118
|
end
|
|
119
119
|
end
|
|
@@ -41,7 +41,9 @@ RSpec.shared_examples "Moxml::NodeSet" do
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
it "compares nodes" do
|
|
44
|
-
|
|
44
|
+
xpath_results = doc.xpath("//child")
|
|
45
|
+
element_children = doc.root.children.select { |c| c.is_a?(Moxml::Element) }
|
|
46
|
+
expect(xpath_results.map(&:native)).to eq(element_children.map(&:native))
|
|
45
47
|
end
|
|
46
48
|
end
|
|
47
49
|
|
|
@@ -86,7 +86,7 @@ RSpec.describe "Moxml lazy parse" do
|
|
|
86
86
|
|
|
87
87
|
it "round-trips through serialize" do
|
|
88
88
|
doc = ctx.parse(xml)
|
|
89
|
-
serialized = doc.to_xml
|
|
89
|
+
serialized = doc.to_xml(indent: 0)
|
|
90
90
|
doc2 = ctx.parse(serialized)
|
|
91
91
|
expect(doc2.root.name).to eq("root")
|
|
92
92
|
expect(doc2.root.children.to_a.size).to eq(2)
|
data/spec/moxml/moxml_spec.rb
CHANGED
|
@@ -23,7 +23,6 @@ RSpec.describe Moxml do
|
|
|
23
23
|
|
|
24
24
|
describe ".configure" do
|
|
25
25
|
around do |example|
|
|
26
|
-
# preserve the original config because it may be changed in examples
|
|
27
26
|
described_class.with_config { example.run }
|
|
28
27
|
end
|
|
29
28
|
|
|
@@ -47,4 +46,10 @@ RSpec.describe Moxml do
|
|
|
47
46
|
expect(context.config.default_encoding).to eq("US-ASCII")
|
|
48
47
|
end
|
|
49
48
|
end
|
|
49
|
+
|
|
50
|
+
describe "Config.runtime_default_adapter" do
|
|
51
|
+
it "returns a valid adapter for the current runtime" do
|
|
52
|
+
expect(Moxml::Config::VALID_ADAPTERS).to include(Moxml::Config.runtime_default_adapter)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
50
55
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require_relative "shared_examples"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Moxml::NativeAttachment::Native do
|
|
7
|
+
it_behaves_like "an attachment backend"
|
|
8
|
+
|
|
9
|
+
it "stores attachments outside native object instance variables" do
|
|
10
|
+
attachments = described_class.new
|
|
11
|
+
native = Object.new
|
|
12
|
+
|
|
13
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
14
|
+
|
|
15
|
+
aggregate_failures do
|
|
16
|
+
expect(attachments.get(native, :entity_refs)).to eq(["amp"])
|
|
17
|
+
expect(native.instance_variables).to be_empty
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "registers one finalizer per native object that clears sidecar storage" do
|
|
22
|
+
attachments = described_class.new
|
|
23
|
+
native = Object.new
|
|
24
|
+
other_native = Object.new
|
|
25
|
+
finalizers = {}.compare_by_identity
|
|
26
|
+
|
|
27
|
+
expect(ObjectSpace).to receive(:define_finalizer)
|
|
28
|
+
.with(native, kind_of(Proc)).once do |object, finalizer|
|
|
29
|
+
finalizers[object] = finalizer
|
|
30
|
+
end
|
|
31
|
+
expect(ObjectSpace).to receive(:define_finalizer)
|
|
32
|
+
.with(other_native, kind_of(Proc)).once do |object, finalizer|
|
|
33
|
+
finalizers[object] = finalizer
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
37
|
+
attachments.set(native, :doctype, "html")
|
|
38
|
+
attachments.set(other_native, :entity_refs, ["lt"])
|
|
39
|
+
|
|
40
|
+
native_id = native.object_id
|
|
41
|
+
data = attachments.instance_variable_get(:@data)
|
|
42
|
+
registered = attachments.instance_variable_get(:@finalizer_registered)
|
|
43
|
+
|
|
44
|
+
aggregate_failures do
|
|
45
|
+
expect(data).to have_key(native_id)
|
|
46
|
+
expect(registered).to have_key(native_id)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
finalizers.fetch(native).call
|
|
50
|
+
|
|
51
|
+
aggregate_failures do
|
|
52
|
+
expect(data).not_to have_key(native_id)
|
|
53
|
+
expect(registered).not_to have_key(native_id)
|
|
54
|
+
expect(attachments.get(other_native, :entity_refs)).to eq(["lt"])
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require_relative "shared_examples"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Moxml::NativeAttachment::Opal do
|
|
7
|
+
it_behaves_like "an attachment backend"
|
|
8
|
+
|
|
9
|
+
it "stores attachments in Moxml-owned instance variables" do
|
|
10
|
+
attachments = described_class.new
|
|
11
|
+
native = Object.new
|
|
12
|
+
|
|
13
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
14
|
+
|
|
15
|
+
expect(native.instance_variable_get(:@moxml_attachment_entity_refs))
|
|
16
|
+
.to eq(["amp"])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "removes the attachment instance variable on delete" do
|
|
20
|
+
attachments = described_class.new
|
|
21
|
+
native = Object.new
|
|
22
|
+
|
|
23
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
24
|
+
attachments.delete(native, :entity_refs)
|
|
25
|
+
|
|
26
|
+
expect(native.instance_variable_defined?(:@moxml_attachment_entity_refs))
|
|
27
|
+
.to be(false)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.shared_examples "an attachment backend" do
|
|
4
|
+
subject(:attachments) { described_class.new }
|
|
5
|
+
|
|
6
|
+
let(:native) { Object.new }
|
|
7
|
+
let(:other_native) { Object.new }
|
|
8
|
+
|
|
9
|
+
it "stores and reads attachments by native object and key" do
|
|
10
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
11
|
+
attachments.set(native, :doctype, "html")
|
|
12
|
+
attachments.set(other_native, :entity_refs, ["lt"])
|
|
13
|
+
|
|
14
|
+
aggregate_failures do
|
|
15
|
+
expect(attachments.get(native, :entity_refs)).to eq(["amp"])
|
|
16
|
+
expect(attachments.get(native, :doctype)).to eq("html")
|
|
17
|
+
expect(attachments.get(other_native, :entity_refs)).to eq(["lt"])
|
|
18
|
+
expect(attachments.key?(native, :entity_refs)).to be(true)
|
|
19
|
+
expect(attachments.key?(native, :missing)).to be(false)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "preserves explicit nil attachments" do
|
|
24
|
+
attachments.set(native, :xml_declaration, nil)
|
|
25
|
+
|
|
26
|
+
aggregate_failures do
|
|
27
|
+
expect(attachments.get(native, :xml_declaration)).to be_nil
|
|
28
|
+
expect(attachments.key?(native, :xml_declaration)).to be(true)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "deletes attachments" do
|
|
33
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
34
|
+
|
|
35
|
+
expect(attachments.delete(native, :entity_refs)).to eq(["amp"])
|
|
36
|
+
|
|
37
|
+
aggregate_failures do
|
|
38
|
+
expect(attachments.get(native, :entity_refs)).to be_nil
|
|
39
|
+
expect(attachments.key?(native, :entity_refs)).to be(false)
|
|
40
|
+
expect(attachments.delete(native, :entity_refs)).to be_nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require "spec_helper"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Moxml::NativeAttachment do
|
|
8
|
+
describe ".default_backend" do
|
|
9
|
+
it "uses the Opal backend under Opal" do
|
|
10
|
+
stub_const("RUBY_ENGINE", "opal")
|
|
11
|
+
|
|
12
|
+
expect(described_class.default_backend).to be_a(described_class::Opal)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "uses the native backend under non-Opal Ruby engines" do
|
|
16
|
+
stub_const("RUBY_ENGINE", "ruby")
|
|
17
|
+
|
|
18
|
+
expect(described_class.default_backend).to be_a(described_class::Native)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe "facade" do
|
|
23
|
+
subject(:attachments) { described_class.new }
|
|
24
|
+
|
|
25
|
+
let(:native) { Object.new }
|
|
26
|
+
|
|
27
|
+
it "delegates storage calls to the configured backend" do
|
|
28
|
+
backend = Class.new do
|
|
29
|
+
attr_reader :calls
|
|
30
|
+
|
|
31
|
+
def initialize
|
|
32
|
+
@calls = []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def get(native, key)
|
|
36
|
+
@calls << [:get, native, key]
|
|
37
|
+
:stored_value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def set(native, key, value)
|
|
41
|
+
@calls << [:set, native, key, value]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def key?(native, key)
|
|
45
|
+
@calls << [:key?, native, key]
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def delete(native, key)
|
|
50
|
+
@calls << [:delete, native, key]
|
|
51
|
+
:stored_value
|
|
52
|
+
end
|
|
53
|
+
end.new
|
|
54
|
+
attachments = described_class.new(backend: backend)
|
|
55
|
+
|
|
56
|
+
expect(attachments.get(native, :entity_refs)).to eq(:stored_value)
|
|
57
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
58
|
+
expect(attachments.key?(native, :entity_refs)).to be(true)
|
|
59
|
+
expect(attachments.delete(native, :entity_refs)).to eq(:stored_value)
|
|
60
|
+
|
|
61
|
+
expect(backend.calls).to eq(
|
|
62
|
+
[
|
|
63
|
+
[:get, native, :entity_refs],
|
|
64
|
+
[:set, native, :entity_refs, ["amp"]],
|
|
65
|
+
[:key?, native, :entity_refs],
|
|
66
|
+
[:delete, native, :entity_refs],
|
|
67
|
+
],
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "uses the selected runtime backend" do
|
|
72
|
+
expected_class = if RUBY_ENGINE == "opal"
|
|
73
|
+
described_class::Opal
|
|
74
|
+
else
|
|
75
|
+
described_class::Native
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
expect(attachments.backend).to be_a(expected_class)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe "loader" do
|
|
83
|
+
let(:lib_dir) { File.expand_path("../../lib", __dir__) }
|
|
84
|
+
let(:ruby) { RbConfig.ruby }
|
|
85
|
+
|
|
86
|
+
it "loads NativeAttachment through the top-level moxml entrypoint" do
|
|
87
|
+
stdout, stderr, status = Open3.capture3(
|
|
88
|
+
ruby,
|
|
89
|
+
"-I",
|
|
90
|
+
lib_dir,
|
|
91
|
+
"-e",
|
|
92
|
+
'require "moxml"; puts Moxml::NativeAttachment.new.respond_to?(:set)',
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
expect(status.success?).to be(true), stderr
|
|
96
|
+
expect(stdout).to eq("true\n")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "loads NativeAttachment through the internal facade file" do
|
|
100
|
+
stdout, stderr, status = Open3.capture3(
|
|
101
|
+
ruby,
|
|
102
|
+
"-I",
|
|
103
|
+
lib_dir,
|
|
104
|
+
"-e",
|
|
105
|
+
'require "moxml/native_attachment"; puts Moxml::NativeAttachment.new.respond_to?(:set)',
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
expect(status.success?).to be(true), stderr
|
|
109
|
+
expect(stdout).to eq("true\n")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "registers backend implementations with require-style autoload paths" do
|
|
113
|
+
stdout, stderr, status = Open3.capture3(
|
|
114
|
+
ruby,
|
|
115
|
+
"-I",
|
|
116
|
+
lib_dir,
|
|
117
|
+
"-e",
|
|
118
|
+
<<~RUBY,
|
|
119
|
+
require "moxml/native_attachment"
|
|
120
|
+
|
|
121
|
+
puts Moxml::NativeAttachment.autoload?(:Opal)
|
|
122
|
+
puts Moxml::NativeAttachment.autoload?(:Native)
|
|
123
|
+
RUBY
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
expect(status.success?).to be(true), stderr
|
|
127
|
+
expect(stdout).to eq(
|
|
128
|
+
"moxml/native_attachment/opal\n" \
|
|
129
|
+
"moxml/native_attachment/native\n",
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "loads the native backend lazily when selected" do
|
|
134
|
+
stdout, stderr, status = Open3.capture3(
|
|
135
|
+
ruby,
|
|
136
|
+
"-I",
|
|
137
|
+
lib_dir,
|
|
138
|
+
"-e",
|
|
139
|
+
<<~'RUBY',
|
|
140
|
+
require "moxml/native_attachment"
|
|
141
|
+
|
|
142
|
+
puts $LOADED_FEATURES.grep(%r{/native_attachment/native\.rb\z}).empty?
|
|
143
|
+
puts Moxml::NativeAttachment.new.backend.class
|
|
144
|
+
puts !$LOADED_FEATURES.grep(%r{/native_attachment/native\.rb\z}).empty?
|
|
145
|
+
puts $LOADED_FEATURES.grep(%r{/native_attachment/opal\.rb\z}).empty?
|
|
146
|
+
RUBY
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
expect(status.success?).to be(true), stderr
|
|
150
|
+
expect(stdout).to eq(
|
|
151
|
+
"true\n" \
|
|
152
|
+
"Moxml::NativeAttachment::Native\n" \
|
|
153
|
+
"true\n" \
|
|
154
|
+
"true\n",
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "does not load the native backend when Opal is selected" do
|
|
159
|
+
stdout, stderr, status = Open3.capture3(
|
|
160
|
+
ruby,
|
|
161
|
+
"-I",
|
|
162
|
+
lib_dir,
|
|
163
|
+
"-e",
|
|
164
|
+
<<~'RUBY',
|
|
165
|
+
Object.send(:remove_const, :RUBY_ENGINE)
|
|
166
|
+
RUBY_ENGINE = "opal"
|
|
167
|
+
|
|
168
|
+
require "moxml/native_attachment"
|
|
169
|
+
|
|
170
|
+
puts Moxml::NativeAttachment.autoload?(:Native)
|
|
171
|
+
puts Moxml::NativeAttachment.new.backend.class
|
|
172
|
+
puts $LOADED_FEATURES.grep(%r{/native_attachment/native\.rb\z}).empty?
|
|
173
|
+
RUBY
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
expect(status.success?).to be(true), stderr
|
|
177
|
+
expect(stdout).to eq(
|
|
178
|
+
"moxml/native_attachment/native\n" \
|
|
179
|
+
"Moxml::NativeAttachment::Opal\n" \
|
|
180
|
+
"true\n",
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -127,7 +127,7 @@ RSpec.describe "XPath Node Functions" do
|
|
|
127
127
|
it "inherits language from parent element" do
|
|
128
128
|
ast = Moxml::XPath::Parser.parse('lang("en")')
|
|
129
129
|
proc = Moxml::XPath::Compiler.compile_with_cache(ast)
|
|
130
|
-
child = doc_with_lang.root.children.first
|
|
130
|
+
child = doc_with_lang.root.children.select { |c| c.is_a?(Moxml::Element) }.first
|
|
131
131
|
result = proc.call(child)
|
|
132
132
|
|
|
133
133
|
expect(result).to be true
|
|
@@ -136,7 +136,8 @@ RSpec.describe "XPath Node Functions" do
|
|
|
136
136
|
it "uses closest xml:lang attribute" do
|
|
137
137
|
ast = Moxml::XPath::Parser.parse('lang("fr")')
|
|
138
138
|
proc = Moxml::XPath::Compiler.compile_with_cache(ast)
|
|
139
|
-
|
|
139
|
+
elements = doc_with_lang.root.children.select { |c| c.is_a?(Moxml::Element) }
|
|
140
|
+
other = elements[1]
|
|
140
141
|
result = proc.call(other)
|
|
141
142
|
|
|
142
143
|
expect(result).to be true
|
|
@@ -27,10 +27,10 @@ RSpec.shared_examples "Performance Examples" do
|
|
|
27
27
|
{
|
|
28
28
|
nokogiri: { parser: 15, serializer: 1000 },
|
|
29
29
|
oga: { parser: 10, serializer: 100 },
|
|
30
|
-
rexml: { parser: 0, serializer:
|
|
30
|
+
rexml: { parser: 0, serializer: 5 },
|
|
31
31
|
ox: { parser: 2, serializer: 1000 },
|
|
32
32
|
headed_ox: { parser: 2, serializer: 1000 },
|
|
33
|
-
libxml: { parser:
|
|
33
|
+
libxml: { parser: 2, serializer: 3 },
|
|
34
34
|
}
|
|
35
35
|
end
|
|
36
36
|
|
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.1.
|
|
4
|
+
version: 0.1.19
|
|
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-
|
|
11
|
+
date: 2026-05-01 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
|
|
@@ -141,6 +141,8 @@ files:
|
|
|
141
141
|
- lib/moxml/error.rb
|
|
142
142
|
- lib/moxml/namespace.rb
|
|
143
143
|
- lib/moxml/native_attachment.rb
|
|
144
|
+
- lib/moxml/native_attachment/native.rb
|
|
145
|
+
- lib/moxml/native_attachment/opal.rb
|
|
144
146
|
- lib/moxml/node.rb
|
|
145
147
|
- lib/moxml/node_set.rb
|
|
146
148
|
- lib/moxml/processing_instruction.rb
|
|
@@ -325,6 +327,10 @@ files:
|
|
|
325
327
|
- spec/moxml/moxml_spec.rb
|
|
326
328
|
- spec/moxml/namespace_spec.rb
|
|
327
329
|
- spec/moxml/namespace_uri_validation_spec.rb
|
|
330
|
+
- spec/moxml/native_attachment/native_spec.rb
|
|
331
|
+
- spec/moxml/native_attachment/opal_spec.rb
|
|
332
|
+
- spec/moxml/native_attachment/shared_examples.rb
|
|
333
|
+
- spec/moxml/native_attachment_spec.rb
|
|
328
334
|
- spec/moxml/node_cache_spec.rb
|
|
329
335
|
- spec/moxml/node_set_cache_spec.rb
|
|
330
336
|
- spec/moxml/node_set_spec.rb
|