moxml 0.1.18 → 0.1.20

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +181 -11
  3. data/README.adoc +24 -1
  4. data/docs/_guides/node-api-consistency.adoc +4 -0
  5. data/lib/moxml/adapter/base.rb +6 -1
  6. data/lib/moxml/adapter/customized_ox/text.rb +15 -2
  7. data/lib/moxml/adapter/customized_rexml/formatter.rb +1 -0
  8. data/lib/moxml/adapter/libxml.rb +7 -2
  9. data/lib/moxml/adapter/oga.rb +6 -2
  10. data/lib/moxml/adapter/ox.rb +15 -8
  11. data/lib/moxml/builder.rb +12 -5
  12. data/lib/moxml/config.rb +1 -1
  13. data/lib/moxml/entity_registry.rb +1 -0
  14. data/lib/moxml/native_attachment/native.rb +69 -0
  15. data/lib/moxml/native_attachment/opal.rb +34 -0
  16. data/lib/moxml/native_attachment.rb +16 -46
  17. data/lib/moxml/text.rb +4 -0
  18. data/lib/moxml/version.rb +1 -1
  19. data/lib/moxml/xpath/compiler.rb +2 -1
  20. data/spec/integration/shared_examples/edge_cases.rb +4 -2
  21. data/spec/integration/shared_examples/entity_reference_whitespace.rb +1 -1
  22. data/spec/integration/shared_examples/high_level/document_builder_behavior.rb +3 -1
  23. data/spec/integration/shared_examples/node_wrappers/entity_reference_behavior.rb +10 -4
  24. data/spec/integration/shared_examples/node_wrappers/node_set_behavior.rb +1 -1
  25. data/spec/moxml/adapter/headed_ox_spec.rb +1 -1
  26. data/spec/moxml/lazy_parse_spec.rb +1 -1
  27. data/spec/moxml/moxml_spec.rb +6 -40
  28. data/spec/moxml/native_attachment/native_spec.rb +57 -0
  29. data/spec/moxml/native_attachment/opal_spec.rb +29 -0
  30. data/spec/moxml/native_attachment/shared_examples.rb +43 -0
  31. data/spec/moxml/native_attachment_spec.rb +184 -0
  32. data/spec/moxml/text_spec.rb +23 -0
  33. data/spec/moxml/xpath/functions/node_functions_spec.rb +3 -2
  34. data/spec/performance/benchmark_spec.rb +1 -1
  35. metadata +8 -2
@@ -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
- def initialize
20
- @data = {}
21
- @finalizer_registered = {}
22
- @monitor = Monitor.new
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
- @monitor.synchronize { @data[native.object_id]&.[](key) }
20
+ @backend.get(native, key)
27
21
  end
28
22
 
29
23
  def set(native, key, value)
30
- id = native.object_id
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
- @monitor.synchronize { @data[native.object_id]&.key?(key) || false }
28
+ @backend.key?(native, key)
40
29
  end
41
30
 
42
31
  def delete(native, key)
43
- @monitor.synchronize { @data[native.object_id]&.delete(key) }
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/text.rb CHANGED
@@ -15,5 +15,9 @@ module Moxml
15
15
  def content=(text)
16
16
  adapter.set_text_content(@native, normalize_xml_value(text))
17
17
  end
18
+
19
+ def to_s
20
+ content
21
+ end
18
22
  end
19
23
  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.1.18"
4
+ VERSION = "0.1.20"
5
5
  end
@@ -1758,7 +1758,8 @@ module Moxml
1758
1758
  current = visit.pop
1759
1759
 
1760
1760
  # Function name is stored in :value field, not children
1761
- if (current.type == :call || current.type == :function) && current.value == name
1761
+ if %i[call
1762
+ function].include?(current.type) && current.value == name
1762
1763
  return true
1763
1764
  end
1764
1765
 
@@ -170,7 +170,9 @@ RSpec.shared_examples "Moxml Edge Cases" do
170
170
  describe "whitespace text node preservation" do
171
171
  # Ox/HeadedOx do not generate whitespace-only text nodes in their parser,
172
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) }
173
+ let(:preserves_ws) do
174
+ !%i[ox headed_ox].include?(context.config.adapter_name)
175
+ end
174
176
 
175
177
  it "preserves whitespace-only text nodes between sibling elements" do
176
178
  unless preserves_ws
@@ -196,7 +198,7 @@ RSpec.shared_examples "Moxml Edge Cases" do
196
198
  expect(ws_nodes).not_to be_empty
197
199
 
198
200
  # Element children should still be accessible
199
- elements = children.select { |c| c.is_a?(Moxml::Element) }
201
+ elements = children.grep(Moxml::Element)
200
202
  expect(elements.map(&:name)).to eq(%w[a b c])
201
203
  end
202
204
 
@@ -116,7 +116,7 @@ RSpec.shared_examples "Entity Reference Whitespace Preservation" do
116
116
  children = doc.root.children
117
117
 
118
118
  # Whitespace text nodes between elements are preserved
119
- elements = children.select { |c| c.is_a?(Moxml::Element) }
119
+ elements = children.grep(Moxml::Element)
120
120
  expect(elements.length).to eq(2)
121
121
  expect(elements.map(&:name)).to eq(%w[child1 child2])
122
122
  end
@@ -38,7 +38,9 @@ RSpec.shared_examples "Moxml::DocumentBuilder" do
38
38
  expect(non_ws_children[1]).to be_a(Moxml::Element)
39
39
  expect(non_ws_children[1].name).to eq("child")
40
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)
41
+ expect(non_ws_children[1].children.find do |c|
42
+ c.is_a?(Moxml::Cdata)
43
+ end).to be_a(Moxml::Cdata)
42
44
  expect(non_ws_children[2]).to be_a(Moxml::ProcessingInstruction)
43
45
  end
44
46
  end
@@ -129,16 +129,20 @@ RSpec.shared_examples "Moxml::EntityReference" do
129
129
 
130
130
  describe "entity restoration" do
131
131
  it "restores standard XML entities when enabled" do
132
- ctx_restore = Moxml.new(context.config.adapter_name) { |c| c.restore_entities = true }
132
+ ctx_restore = Moxml.new(context.config.adapter_name) do |c|
133
+ c.restore_entities = true
134
+ end
133
135
  doc = ctx_restore.parse("<p>a&amp;b</p>")
134
136
  output = doc.to_xml
135
137
  expect(output).to include("&amp;")
136
138
  end
137
139
 
138
140
  it "does not create entity references when disabled" do
139
- ctx_no_restore = Moxml.new(context.config.adapter_name) { |c| c.restore_entities = false }
141
+ ctx_no_restore = Moxml.new(context.config.adapter_name) do |c|
142
+ c.restore_entities = false
143
+ end
140
144
  doc = ctx_no_restore.parse("<p>text</p>")
141
- refs = doc.root.children.select { |c| c.is_a?(Moxml::EntityReference) }
145
+ refs = doc.root.children.grep(Moxml::EntityReference)
142
146
  expect(refs).to be_empty
143
147
  end
144
148
  end
@@ -197,7 +201,9 @@ RSpec.shared_examples "Moxml::EntityReference" do
197
201
 
198
202
  it "rejects invalid modes" do
199
203
  cfg = Moxml::Config.new(context.config.adapter_name)
200
- expect { cfg.entity_restoration_mode = :bogus }.to raise_error(ArgumentError)
204
+ expect do
205
+ cfg.entity_restoration_mode = :bogus
206
+ end.to raise_error(ArgumentError)
201
207
  end
202
208
 
203
209
  it "restores non-standard entities in lenient mode" do
@@ -42,7 +42,7 @@ RSpec.shared_examples "Moxml::NodeSet" do
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) }
45
+ element_children = doc.root.children.grep(Moxml::Element)
46
46
  expect(xpath_results.map(&:native)).to eq(element_children.map(&:native))
47
47
  end
48
48
  end
@@ -145,7 +145,7 @@ RSpec.describe Moxml::Adapter::HeadedOx do
145
145
  it "returns first matching node" do
146
146
  result = adapter.at_xpath(doc, "//book")
147
147
 
148
- expect(result).to be_a(::Ox::Element)
148
+ expect(result).to be_a(Ox::Element)
149
149
  expect(result.name).to eq("book")
150
150
  end
151
151
 
@@ -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)
@@ -2,28 +2,6 @@
2
2
 
3
3
  # spec/moxml_spec.rb
4
4
  RSpec.describe Moxml do
5
- around do |example|
6
- original_default = Moxml::Config.instance_variable_get(:@default)
7
- original_default_adapter = Moxml::Config.instance_variable_get(:@default_adapter)
8
-
9
- Moxml::Config.remove_instance_variable(:@default) if Moxml::Config.instance_variable_defined?(:@default)
10
- Moxml::Config.remove_instance_variable(:@default_adapter) if Moxml::Config.instance_variable_defined?(:@default_adapter)
11
-
12
- example.run
13
- ensure
14
- if original_default.nil?
15
- Moxml::Config.remove_instance_variable(:@default) if Moxml::Config.instance_variable_defined?(:@default)
16
- else
17
- Moxml::Config.instance_variable_set(:@default, original_default)
18
- end
19
-
20
- if original_default_adapter.nil?
21
- Moxml::Config.remove_instance_variable(:@default_adapter) if Moxml::Config.instance_variable_defined?(:@default_adapter)
22
- else
23
- Moxml::Config.instance_variable_set(:@default_adapter, original_default_adapter)
24
- end
25
- end
26
-
27
5
  it "has a version number" do
28
6
  expect(Moxml::VERSION).not_to be_nil
29
7
  end
@@ -45,7 +23,6 @@ RSpec.describe Moxml do
45
23
 
46
24
  describe ".configure" do
47
25
  around do |example|
48
- # preserve the original config because it may be changed in examples
49
26
  described_class.with_config { example.run }
50
27
  end
51
28
 
@@ -56,23 +33,6 @@ RSpec.describe Moxml do
56
33
  expect(context.config.adapter_name).to eq(:nokogiri)
57
34
  end
58
35
 
59
- it "defaults to oga on Opal" do
60
- stub_const("RUBY_ENGINE", "opal")
61
-
62
- context = described_class.new
63
- expect(context.config.adapter_name).to eq(:oga)
64
- end
65
-
66
- it "prefers ox when it is already loaded" do
67
- allow(Object).to receive(:const_defined?).and_call_original
68
- allow(Object).to receive(:const_defined?).with(:Nokogiri).and_return(false)
69
- allow(Object).to receive(:const_defined?).with(:Ox).and_return(true)
70
- allow(Object).to receive(:const_defined?).with(:Oga).and_return(false)
71
-
72
- context = described_class.new
73
- expect(context.config.adapter_name).to eq(:ox)
74
- end
75
-
76
36
  it "uses configured options from the block" do
77
37
  described_class.configure do |config|
78
38
  config.default_adapter = :oga
@@ -86,4 +46,10 @@ RSpec.describe Moxml do
86
46
  expect(context.config.default_encoding).to eq("US-ASCII")
87
47
  end
88
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
89
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
@@ -19,6 +19,29 @@ RSpec.describe Moxml::Text do
19
19
  text = doc.root.children.first
20
20
  expect(text.to_xml).to eq("plain text")
21
21
  end
22
+
23
+ it "escapes XML special characters" do
24
+ escaped_doc = context.parse("<root>a &lt; b &amp; c</root>")
25
+ text = escaped_doc.root.children.first
26
+ expect(text.to_xml).to eq("a &lt; b &amp; c")
27
+ end
28
+ end
29
+
30
+ describe "#to_s" do
31
+ it "returns text content" do
32
+ text = doc.root.children.first
33
+ expect(text.to_s).to eq("plain text")
34
+ end
35
+
36
+ it "is consistent across adapters" do
37
+ Moxml::Adapter::AVALIABLE_ADAPTERS.each do |adapter_name|
38
+ ctx = Moxml.new(adapter_name)
39
+ d = ctx.parse("<root>hello world</root>")
40
+ text = d.root.children.first
41
+ expect(text.to_s).to eq("hello world"),
42
+ "Text#to_s for #{adapter_name} adapter"
43
+ end
44
+ end
22
45
  end
23
46
 
24
47
  describe "creation" do