moxml 0.1.18 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1daf227e9effc582c66e780516135481aa48e467226a4267974633a4673f786
4
- data.tar.gz: 5b230e79a208eb4b1c5e32175df364e5b42f9e766c4c8189eb18fcad09bb79bf
3
+ metadata.gz: 76508cc0d2699469ade87f48e38ea73289abc4f95cb7a313f6d71548477248f3
4
+ data.tar.gz: 1752cb433953a869cedd1d88fc565c53f77bd08201c5977247150e1e7b75a386
5
5
  SHA512:
6
- metadata.gz: e39941f6f51567655c246f1e8d6225c6ab572c68958fd9ca4a74ca191af4493e1bf74ff0feed84792cc87e5681cce1d8ae291533e992d07b70fdfb1063d96a67
7
- data.tar.gz: 41ca4a3954bf2e713703124758309e5d1e056d44826cdacf3b4693c5bf37c463579462a6c0ecb3aa65e4c3cafaeef1e2ad0d8a5af9ebe2e1b9f4a695e7678ff4
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:: (default) https://github.com/sparklemotion/nokogiri[Nokogiri], a
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/config.rb CHANGED
@@ -21,7 +21,7 @@ module Moxml
21
21
  end
22
22
 
23
23
  def default_adapter
24
- @default_adapter ||= runtime_default_adapter
24
+ @default_adapter || runtime_default_adapter
25
25
  end
26
26
 
27
27
  def runtime_default_adapter
@@ -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
- 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/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.19"
5
5
  end
@@ -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
@@ -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
- other = doc_with_lang.root.children[1]
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
@@ -30,7 +30,7 @@ RSpec.shared_examples "Performance Examples" do
30
30
  rexml: { parser: 0, serializer: 5 },
31
31
  ox: { parser: 2, serializer: 1000 },
32
32
  headed_ox: { parser: 2, serializer: 1000 },
33
- libxml: { parser: 10, serializer: 30 },
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.18
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-04-27 00:00:00.000000000 Z
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