moxml 0.1.21 → 0.1.23

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/opal.yml +37 -0
  3. data/.gitignore +1 -0
  4. data/.rspec-opal +5 -0
  5. data/.rubocop.yml +1 -0
  6. data/.rubocop_todo.yml +680 -110
  7. data/Gemfile +6 -0
  8. data/Rakefile +70 -0
  9. data/lib/compat/opal/rexml/namespace.rb +59 -0
  10. data/lib/compat/opal/rexml/parsers/baseparser.rb +1016 -0
  11. data/lib/compat/opal/rexml/source.rb +214 -0
  12. data/lib/compat/opal/rexml/text.rb +426 -0
  13. data/lib/compat/opal/rexml/xmltokens.rb +45 -0
  14. data/lib/compat/opal/rexml_compat.rb +77 -0
  15. data/lib/moxml/adapter/customized_oga/xml_declaration.rb +8 -1
  16. data/lib/moxml/adapter/customized_rexml/formatter.rb +11 -10
  17. data/lib/moxml/adapter/headed_ox.rb +2 -6
  18. data/lib/moxml/adapter/libxml/entity_ref_registry.rb +4 -2
  19. data/lib/moxml/adapter/libxml/entity_restorer.rb +3 -1
  20. data/lib/moxml/adapter/libxml.rb +22 -24
  21. data/lib/moxml/adapter/nokogiri.rb +24 -33
  22. data/lib/moxml/adapter/oga.rb +47 -84
  23. data/lib/moxml/adapter/ox.rb +43 -41
  24. data/lib/moxml/adapter/rexml.rb +29 -33
  25. data/lib/moxml/adapter.rb +38 -8
  26. data/lib/moxml/config.rb +16 -3
  27. data/lib/moxml/document.rb +2 -8
  28. data/lib/moxml/entity_registry.rb +40 -31
  29. data/lib/moxml/entity_registry_opal_data.rb +2138 -0
  30. data/lib/moxml/node.rb +27 -26
  31. data/lib/moxml/sax/namespace_splitter.rb +54 -0
  32. data/lib/moxml/version.rb +1 -1
  33. data/lib/moxml/xml_utils.rb +10 -1
  34. data/lib/moxml.rb +7 -0
  35. data/spec/consistency/adapter_parity_spec.rb +1 -1
  36. data/spec/integration/all_adapters_spec.rb +2 -1
  37. data/spec/integration/shared_examples/line_ending_behavior.rb +56 -0
  38. data/spec/integration/w3c_namespace_spec.rb +1 -1
  39. data/spec/moxml/adapter/libxml_internals_spec.rb +4 -2
  40. data/spec/moxml/adapter/ox_spec.rb +8 -0
  41. data/spec/moxml/adapter/platform_spec.rb +70 -0
  42. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -6
  43. data/spec/moxml/config_spec.rb +33 -0
  44. data/spec/moxml/entity_registry_spec.rb +10 -0
  45. data/spec/moxml/native_attachment/opal_spec.rb +39 -2
  46. data/spec/moxml/node_type_map_spec.rb +43 -0
  47. data/spec/moxml/opal_rexml_adapter_spec.rb +14 -0
  48. data/spec/moxml/opal_smoke_spec.rb +61 -0
  49. data/spec/moxml/sax/namespace_splitter_spec.rb +67 -0
  50. data/spec/moxml/text_spec.rb +1 -1
  51. data/spec/spec_helper.rb +32 -13
  52. data/spec/support/opal.rb +16 -0
  53. metadata +19 -2
data/lib/moxml/node.rb CHANGED
@@ -98,6 +98,7 @@ module Moxml
98
98
  serialize_options[:no_declaration] = !should_include_declaration?(options)
99
99
 
100
100
  result = adapter.serialize(@native, serialize_options)
101
+ result = apply_line_ending(result, serialize_options[:line_ending])
101
102
 
102
103
  # Restore entity markers to named entity references
103
104
  adapter.restore_entities(result)
@@ -135,19 +136,6 @@ module Moxml
135
136
  children.last
136
137
  end
137
138
 
138
- # Returns the text content of this node
139
- # For elements, returns concatenated text of all text children
140
- # For text nodes, returns the content if available
141
- def text
142
- if respond_to?(:content)
143
- content
144
- elsif respond_to?(:children)
145
- children.grep(Text).map(&:content).join
146
- else
147
- ""
148
- end
149
- end
150
-
151
139
  # Returns the text content of this node
152
140
  # Subclasses should override this method
153
141
  # Element and Text have their own implementations
@@ -220,22 +208,28 @@ module Moxml
220
208
  nil
221
209
  end
222
210
 
211
+ # Registry mapping node type symbols to wrapper classes.
212
+ # Built lazily to avoid load-order issues with subclasses.
213
+ def self.node_type_map
214
+ @node_type_map ||= {
215
+ element: Element,
216
+ text: Text,
217
+ cdata: Cdata,
218
+ comment: Comment,
219
+ processing_instruction: ProcessingInstruction,
220
+ document: Document,
221
+ declaration: Declaration,
222
+ doctype: Doctype,
223
+ attribute: Attribute,
224
+ entity_reference: EntityReference,
225
+ }.freeze
226
+ end
227
+
223
228
  def self.wrap(node, context)
224
229
  return nil if node.nil?
225
230
 
226
- klass = case adapter(context).node_type(node)
227
- when :element then Element
228
- when :text then Text
229
- when :cdata then Cdata
230
- when :comment then Comment
231
- when :processing_instruction then ProcessingInstruction
232
- when :document then Document
233
- when :declaration then Declaration
234
- when :doctype then Doctype
235
- when :attribute then Attribute
236
- when :entity_reference then EntityReference
237
- else self
238
- end
231
+ type = adapter(context).node_type(node)
232
+ klass = node_type_map[type] || self
239
233
 
240
234
  klass.new(node, context)
241
235
  end
@@ -286,6 +280,7 @@ module Moxml
286
280
  {
287
281
  encoding: context.config.default_encoding,
288
282
  indent: context.config.default_indent,
283
+ line_ending: context.config.default_line_ending,
289
284
  # The short format of empty tags in Oga and Nokogiri isn't configurable
290
285
  # Oga: <empty /> (with a space)
291
286
  # Nokogiri: <empty/> (without a space)
@@ -301,5 +296,11 @@ module Moxml
301
296
  # For Document nodes, delegate to adapter for native state check
302
297
  adapter.has_declaration?(@native, self)
303
298
  end
299
+
300
+ def apply_line_ending(xml, line_ending)
301
+ return xml if line_ending == Config::LINE_ENDING_LF || !xml.include?("\n")
302
+
303
+ xml.gsub(/\r?\n/, line_ending)
304
+ end
304
305
  end
305
306
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module SAX
5
+ # Splits a flat attribute hash into regular attributes and namespace declarations.
6
+ #
7
+ # Every adapter SAX bridge performs the same split: attributes whose names
8
+ # start with "xmlns" are namespace declarations, everything else is a regular
9
+ # attribute. This module provides a single implementation.
10
+ module NamespaceSplitter
11
+ # @param attributes [Hash, Array<Array>] attributes as a hash or array of pairs
12
+ # @return [Array(Hash, Hash)] [regular_attrs, namespaces]
13
+ def split_attributes_and_namespaces(attributes)
14
+ attrs = {}
15
+ ns = {}
16
+
17
+ each_attribute(attributes) do |name, value|
18
+ name_s = name.to_s
19
+ if name_s == "xmlns" || name_s.start_with?("xmlns:")
20
+ prefix = name_s == "xmlns" ? nil : name_s.sub("xmlns:", "")
21
+ ns[prefix] = value
22
+ else
23
+ attrs[name_s] = value
24
+ end
25
+ end
26
+
27
+ [attrs, ns]
28
+ end
29
+
30
+ private
31
+
32
+ def each_attribute(attributes, &block)
33
+ case attributes
34
+ when Hash
35
+ attributes.each(&block)
36
+ when Array
37
+ attributes.each { |pair| yield pair[0], pair[1] }
38
+ when nil
39
+ # nothing
40
+ else
41
+ if attributes.respond_to?(:each)
42
+ attributes.each do |item|
43
+ if item.is_a?(Array) && item.size >= 2
44
+ yield item[0], item[1]
45
+ elsif item.respond_to?(:name) && item.respond_to?(:value)
46
+ yield item.name, item.value
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ 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.21"
4
+ VERSION = "0.1.23"
5
5
  end
@@ -75,7 +75,16 @@ module Moxml
75
75
  # (W3C Namespaces in XML, https://www.w3.org/TR/xml-names/).
76
76
  # Use split instead of parse to avoid scheme-specific validation
77
77
  # that rejects valid opaque URIs like "mailto:bar".
78
- URI::RFC3986_PARSER.split(uri)
78
+ if defined?(URI::RFC3986_PARSER)
79
+ URI::RFC3986_PARSER.split(uri)
80
+ elsif uri.match?(/\A[a-zA-Z][a-zA-Z0-9+\-.]*:[^\x00-\x20]*\z/)
81
+ # Minimal URI validation for Opal (no RFC3986_PARSER available)
82
+ else
83
+ # Accept relative references and bare paths
84
+ return unless uri.match?(/[\x00-\x08\x0B\x0C\x0E-\x1F]/)
85
+
86
+ raise ValidationError, "Invalid URI: #{uri}"
87
+ end
79
88
  rescue URI::InvalidURIError
80
89
  raise ValidationError, "Invalid URI: #{uri}"
81
90
  end
data/lib/moxml.rb CHANGED
@@ -32,6 +32,13 @@ module Moxml
32
32
  end
33
33
  original_config = nil
34
34
  end
35
+ def preprocess_entities(xml)
36
+ Adapter::Base.preprocess_entities(xml)
37
+ end
38
+
39
+ def restore_entities(text)
40
+ Adapter::Base.restore_entities(text)
41
+ end
35
42
  end
36
43
  end
37
44
 
@@ -6,7 +6,7 @@ RSpec.describe "Adapter Examples" do
6
6
  describe "Serialization consistency" do
7
7
  it "produces equivalent XML across adapters",
8
8
  skip: "No easy way to exclude the declaration from Nokogiri documents" do
9
- docs = Moxml::Adapter::AVALIABLE_ADAPTERS.map do |adapter|
9
+ docs = Moxml::Adapter::AVAILABLE_ADAPTERS.map do |adapter|
10
10
  Moxml.new(adapter).parse(xml, fragment: true)
11
11
  end
12
12
 
@@ -29,10 +29,11 @@ RSpec.describe "Cross-adapter integration" do
29
29
  "Memory Usage Examples",
30
30
  "Thread Safety Examples",
31
31
  "Entity Reference Whitespace Preservation",
32
+ "Moxml Line Ending",
32
33
  "Performance Examples",
33
34
  ]
34
35
 
35
- Moxml::Adapter::AVALIABLE_ADAPTERS.each do |adapter_name|
36
+ Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
36
37
  context "with #{adapter_name}" do
37
38
  around do |example|
38
39
  Moxml.with_config(adapter_name) do
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples "Moxml Line Ending" do
4
+ describe "Line ending configuration" do
5
+ let(:context) { Moxml.new }
6
+ let(:xml) { "<root><child>text</child></root>" }
7
+
8
+ it "produces no CRLF with LF default" do
9
+ doc = context.parse(xml)
10
+ expect(doc.to_xml).not_to include("\r\n")
11
+ end
12
+
13
+ it "produces no bare LF with CRLF configured" do
14
+ context.config.default_line_ending = Moxml::Config::LINE_ENDING_CRLF
15
+ doc = context.parse(xml)
16
+ expect(doc.to_xml).not_to match(/(?<!\r)\n/)
17
+ end
18
+
19
+ it "allows per-call CRLF override producing no bare LF" do
20
+ doc = context.parse(xml)
21
+ output = doc.to_xml(line_ending: Moxml::Config::LINE_ENDING_CRLF)
22
+ expect(output).not_to match(/(?<!\r)\n/)
23
+ end
24
+
25
+ it "per-call LF override wins over config CRLF" do
26
+ context.config.default_line_ending = Moxml::Config::LINE_ENDING_CRLF
27
+ doc = context.parse(xml)
28
+ expect(doc.to_xml(line_ending: Moxml::Config::LINE_ENDING_LF))
29
+ .not_to include("\r\n")
30
+ end
31
+
32
+ it "produces identical bytes on re-serialization with CRLF" do
33
+ context.config.default_line_ending = Moxml::Config::LINE_ENDING_CRLF
34
+ doc = context.parse(xml)
35
+ first = doc.to_xml
36
+
37
+ ctx2 = Moxml.new
38
+ ctx2.config.default_line_ending = Moxml::Config::LINE_ENDING_CRLF
39
+ result = ctx2.parse(first)
40
+ expect(result.to_xml).to eq(first)
41
+ end
42
+
43
+ it "preserves element structure through CRLF round-trip" do
44
+ doc = context.parse("<root><a>text</a><b>more</b></root>")
45
+ context.config.default_line_ending = Moxml::Config::LINE_ENDING_CRLF
46
+ crlf_output = doc.to_xml
47
+
48
+ ctx2 = Moxml.new
49
+ result = ctx2.parse(crlf_output)
50
+ elements = result.root.children.select(&:element?)
51
+ expect(elements.map(&:name)).to eq(%w[a b])
52
+ expect(elements[0].children.first.content).to eq("text")
53
+ expect(elements[1].children.first.content).to eq("more")
54
+ end
55
+ end
56
+ end
@@ -11,7 +11,7 @@
11
11
  # invalid - validity error; non-validating parsers should accept
12
12
 
13
13
  RSpec.describe "W3C XML Namespaces 1.0 test suite" do
14
- Moxml::Adapter::AVALIABLE_ADAPTERS.each do |adapter_name|
14
+ Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
15
15
  context "with #{adapter_name}" do
16
16
  around do |example|
17
17
  Moxml.with_config(adapter_name) do
@@ -105,7 +105,8 @@ RSpec.describe Moxml::Adapter::Libxml do
105
105
 
106
106
  it "returns [nil, nil] when the document has no entity-ref attachments" do
107
107
  root = libxml_native(context.parse("<root><a/></root>").root)
108
- expect(adapter.send(:lookup_entity_ref_serialization, root)).to eq([nil, nil])
108
+ expect(adapter.send(:lookup_entity_ref_serialization,
109
+ root)).to eq([nil, nil])
109
110
  end
110
111
 
111
112
  it "returns [nil, nil] for an element with no entity refs even when the doc has erefs elsewhere" do
@@ -129,7 +130,8 @@ RSpec.describe Moxml::Adapter::Libxml do
129
130
  )
130
131
  a.add_child(eref)
131
132
 
132
- refs, seq = adapter.send(:lookup_entity_ref_serialization, libxml_native(a))
133
+ refs, seq = adapter.send(:lookup_entity_ref_serialization,
134
+ libxml_native(a))
133
135
  expect(refs).to be_an(Array).and(satisfy { |r| !r.empty? })
134
136
  expect(seq).to be_an(Array).and(include(:eref))
135
137
  end
@@ -11,6 +11,14 @@ RSpec.describe Moxml::Adapter::Ox do
11
11
 
12
12
  it_behaves_like "xml adapter"
13
13
 
14
+ describe "node_type" do
15
+ it "returns :namespace for CustomizedOx::Namespace nodes" do
16
+ element = described_class.create_native_element("test")
17
+ ns = described_class.create_native_namespace(element, "ns", "http://example.com")
18
+ expect(described_class.node_type(ns)).to eq(:namespace)
19
+ end
20
+ end
21
+
14
22
  describe "text handling" do
15
23
  let(:doc) { described_class.create_document }
16
24
  let(:element) { described_class.create_native_element("test") }
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Adapter, ".platform_adapters" do
6
+ it "includes all known adapters under MRI" do
7
+ expect(described_class.platform_adapters).to include(:nokogiri, :oga,
8
+ :rexml, :ox)
9
+ end
10
+
11
+ it "uses the AVAILABLE_ADAPTERS constant under MRI" do
12
+ expect(described_class.platform_adapters).to eq(Moxml::Adapter::AVAILABLE_ADAPTERS)
13
+ end
14
+ end
15
+
16
+ RSpec.describe Moxml::Adapter, ".available?" do
17
+ it "returns true for :oga" do
18
+ expect(described_class.available?(:oga)).to be true
19
+ end
20
+
21
+ it "returns true for :nokogiri under MRI" do
22
+ expect(described_class.available?(:nokogiri)).to be true
23
+ end
24
+
25
+ context "when Opal platform adapters are in effect" do
26
+ before do
27
+ allow(described_class).to receive(:platform_adapters)
28
+ .and_return(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
29
+ end
30
+
31
+ it "returns false for :nokogiri" do
32
+ expect(described_class.available?(:nokogiri)).to be false
33
+ end
34
+
35
+ it "returns true for :rexml" do
36
+ expect(described_class.available?(:rexml)).to be true
37
+ end
38
+ end
39
+ end
40
+
41
+ RSpec.describe Moxml::Adapter, ".load" do
42
+ context "when Opal platform adapters are in effect" do
43
+ before do
44
+ allow(described_class).to receive(:platform_adapters)
45
+ .and_return(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
46
+ end
47
+
48
+ it "raises AdapterError for :nokogiri" do
49
+ expect { described_class.load(:nokogiri) }.to raise_error(
50
+ Moxml::AdapterError, /not available on this platform/
51
+ )
52
+ end
53
+ end
54
+ end
55
+
56
+ RSpec.describe "Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS" do
57
+ it "contains only :rexml" do
58
+ expect(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS).to eq(%i[rexml])
59
+ end
60
+ end
61
+
62
+ RSpec.describe "Moxml::Adapter::CONST_NAME_MAP" do
63
+ it "maps :headed_ox to HeadedOx" do
64
+ expect(Moxml::Adapter::CONST_NAME_MAP[:headed_ox]).to eq("HeadedOx")
65
+ end
66
+
67
+ it "falls back to capitalize for unmapped adapters" do
68
+ expect(Moxml::Adapter::CONST_NAME_MAP[:nokogiri]).to be_nil
69
+ end
70
+ end
@@ -159,9 +159,6 @@ RSpec.shared_examples "xml adapter" do
159
159
  if described_class.name.include?("Oga")
160
160
  pending("Oga does not support indentation settings")
161
161
  end
162
- if described_class.name.include?("Rexml")
163
- pending("Postponed for Rexml till better times")
164
- end
165
162
  if described_class.name.include?("Libxml")
166
163
  skip("LibXML serialization does not support indentation (documented limitation)")
167
164
  end
@@ -219,9 +216,6 @@ RSpec.shared_examples "xml adapter" do
219
216
  end
220
217
 
221
218
  it "preserves and correctly handles multiple namespaces" do
222
- if described_class.name.include?("Rexml")
223
- pending("Rexml does not respect ZPath namespaces")
224
- end
225
219
  # Parse original XML
226
220
  doc = described_class.parse(xml).native
227
221
 
@@ -10,6 +10,7 @@ RSpec.describe Moxml::Config do
10
10
  expect(config.strict_parsing).to be true
11
11
  expect(config.default_encoding).to eq("UTF-8")
12
12
  expect(config.default_indent).to eq(2)
13
+ expect(config.default_line_ending).to eq("\n")
13
14
  expect(config.entity_encoding).to eq(:basic)
14
15
  end
15
16
 
@@ -89,6 +90,38 @@ RSpec.describe Moxml::Config do
89
90
  end
90
91
  end
91
92
 
93
+ describe "#default_line_ending=" do
94
+ it "accepts LINE_ENDING_LF" do
95
+ config.default_line_ending = Moxml::Config::LINE_ENDING_LF
96
+ expect(config.default_line_ending).to eq("\n")
97
+ end
98
+
99
+ it "accepts LINE_ENDING_CRLF" do
100
+ config.default_line_ending = Moxml::Config::LINE_ENDING_CRLF
101
+ expect(config.default_line_ending).to eq("\r\n")
102
+ end
103
+
104
+ it "rejects nil" do
105
+ expect { config.default_line_ending = nil }
106
+ .to raise_error(ArgumentError, /Invalid line_ending/)
107
+ end
108
+
109
+ it "rejects arbitrary strings" do
110
+ expect { config.default_line_ending = "BAD" }
111
+ .to raise_error(ArgumentError, /Invalid line_ending/)
112
+ end
113
+
114
+ it "rejects bare CR" do
115
+ expect { config.default_line_ending = "\r" }
116
+ .to raise_error(ArgumentError, /Invalid line_ending/)
117
+ end
118
+
119
+ it "rejects empty string" do
120
+ expect { config.default_line_ending = "" }
121
+ .to raise_error(ArgumentError, /Invalid line_ending/)
122
+ end
123
+ end
124
+
92
125
  describe "#adapter=" do
93
126
  it "sets valid adapter" do
94
127
  config.adapter = :ox
@@ -180,6 +180,16 @@ RSpec.describe Moxml::EntityRegistry do
180
180
  registry = described_class.new
181
181
  expect(registry.load_all).to be(registry)
182
182
  end
183
+
184
+ it "emits deprecation warnings" do
185
+ registry = described_class.new
186
+ [-> { registry.load_html5 },
187
+ -> { registry.load_mathml },
188
+ -> { registry.load_iso },
189
+ -> { registry.load_all }].each do |callable|
190
+ expect { callable.call }.to output.to_stderr
191
+ end
192
+ end
183
193
  end
184
194
 
185
195
  describe "#standard_entity?" do
@@ -1,10 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "spec_helper"
4
- require_relative "shared_examples"
5
4
 
6
5
  RSpec.describe Moxml::NativeAttachment::Opal do
7
- it_behaves_like "an attachment backend"
6
+ subject(:attachments) { described_class.new }
7
+
8
+ let(:native) { Object.new }
9
+ let(:other_native) { Object.new }
10
+
11
+ it "stores and reads attachments by native object and key" do
12
+ attachments.set(native, :entity_refs, ["amp"])
13
+ attachments.set(native, :doctype, "html")
14
+ attachments.set(other_native, :entity_refs, ["lt"])
15
+
16
+ aggregate_failures do
17
+ expect(attachments.get(native, :entity_refs)).to eq(["amp"])
18
+ expect(attachments.get(native, :doctype)).to eq("html")
19
+ expect(attachments.get(other_native, :entity_refs)).to eq(["lt"])
20
+ expect(attachments.key?(native, :entity_refs)).to be(true)
21
+ expect(attachments.key?(native, :missing)).to be(false)
22
+ end
23
+ end
24
+
25
+ it "preserves explicit nil attachments" do
26
+ attachments.set(native, :xml_declaration, nil)
27
+
28
+ aggregate_failures do
29
+ expect(attachments.get(native, :xml_declaration)).to be_nil
30
+ expect(attachments.key?(native, :xml_declaration)).to be(true)
31
+ end
32
+ end
33
+
34
+ it "deletes attachments" do
35
+ attachments.set(native, :entity_refs, ["amp"])
36
+
37
+ expect(attachments.delete(native, :entity_refs)).to eq(["amp"])
38
+
39
+ aggregate_failures do
40
+ expect(attachments.get(native, :entity_refs)).to be_nil
41
+ expect(attachments.key?(native, :entity_refs)).to be(false)
42
+ expect(attachments.delete(native, :entity_refs)).to be_nil
43
+ end
44
+ end
8
45
 
9
46
  it "stores attachments in Moxml-owned instance variables" do
10
47
  attachments = described_class.new
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Node, "node_type_map" do
6
+ it "maps every type symbol to a Node subclass" do
7
+ described_class.node_type_map.each_value do |klass|
8
+ expect(klass < described_class).to be(true), "Expected #{klass} < Moxml::Node"
9
+ end
10
+ end
11
+
12
+ it "covers all standard node types" do
13
+ expected = %i[element text cdata comment processing_instruction
14
+ document declaration doctype attribute entity_reference]
15
+ expect(described_class.node_type_map.keys).to match_array(expected)
16
+ end
17
+
18
+ it "is frozen to prevent modification" do
19
+ expect(described_class.node_type_map).to be_frozen
20
+ end
21
+
22
+ describe ".wrap" do
23
+ let(:ctx) { Moxml.new(:nokogiri) }
24
+
25
+ it "returns nil for nil input" do
26
+ expect(described_class.wrap(nil, ctx)).to be_nil
27
+ end
28
+
29
+ it "wraps native nodes using node_type_map" do
30
+ doc = ctx.parse("<root><child>text</child></root>")
31
+ root = doc.root
32
+ expect(root).to be_a(Moxml::Element)
33
+
34
+ text_node = root.children.first.children.first
35
+ expect(text_node).to be_a(Moxml::Text)
36
+ end
37
+
38
+ it "returns a Node for unknown types" do
39
+ node = described_class.wrap(Object.new, ctx)
40
+ expect(node).to be_a(described_class)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/adapter/shared_examples/adapter_contract"
5
+
6
+ RSpec.describe Moxml::Adapter::Rexml, if: RUBY_ENGINE == "opal" do
7
+ around do |example|
8
+ Moxml.with_config(:rexml, true, "UTF-8") do
9
+ example.run
10
+ end
11
+ end
12
+
13
+ it_behaves_like "xml adapter"
14
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Moxml Opal smoke test", if: RUBY_ENGINE == "opal" do
6
+ let(:context) { Moxml.new(:rexml) }
7
+
8
+ it "parses XML string to document" do
9
+ doc = context.parse("<root><child>text</child></root>")
10
+ expect(doc).not_to be_nil
11
+ root = doc.root
12
+ expect(root.name).to eq("root")
13
+ expect(root.children.first.name).to eq("child")
14
+ end
15
+
16
+ it "round-trips parse and serialize" do
17
+ xml = '<person name="Alice"><age>30</age></person>'
18
+ doc = context.parse(xml)
19
+ serialized = doc.to_xml
20
+ expect(serialized).to include("person")
21
+ expect(serialized).to include("Alice")
22
+ expect(serialized).to include("30")
23
+ end
24
+
25
+ it "builds XML document from scratch" do
26
+ doc = context.create_document
27
+ root = doc.create_element("root")
28
+ root["attr"] = "value"
29
+ text = doc.create_text("hello")
30
+ root.add_child(text)
31
+ doc.add_child(root)
32
+
33
+ expect(doc.to_xml).to include('attr="value"')
34
+ expect(doc.to_xml).to include("hello")
35
+ end
36
+
37
+ it "handles namespaces" do
38
+ xml = '<root xmlns:ns="http://example.com"><ns:child>data</ns:child></root>'
39
+ doc = context.parse(xml)
40
+ expect(doc.to_xml).to include("ns:child")
41
+ end
42
+
43
+ it "handles CDATA sections" do
44
+ xml = "<root><![CDATA[<script>alert('xss')</script>]]></root>"
45
+ doc = context.parse(xml)
46
+ expect(doc.to_xml).to include("<script>")
47
+ end
48
+
49
+ it "handles comments" do
50
+ xml = "<root><!-- a comment --><child/></root>"
51
+ doc = context.parse(xml)
52
+ expect(doc.to_xml).to include("a comment")
53
+ end
54
+
55
+ it "handles processing instructions" do
56
+ xml = '<?xml version="1.0"?><?pi-target pi-data?><root/>'
57
+ doc = context.parse(xml)
58
+ serialized = doc.to_xml
59
+ expect(serialized).to include("pi-target")
60
+ end
61
+ end