moxml 0.1.23 → 0.1.25
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/.github/workflows/docs.yml +1 -1
- data/.github/workflows/opal.yml +18 -1
- data/.github/workflows/rake.yml +32 -3
- data/.github/workflows/round-trip.yml +72 -9
- data/.gitmodules +6 -0
- data/.rubocop_todo.yml +2 -0
- data/Gemfile +9 -1
- data/README.adoc +124 -0
- data/Rakefile +117 -5
- data/lib/compat/opal/moxml_boot.rb +57 -0
- data/lib/moxml/adapter/base.rb +5 -5
- data/lib/moxml/adapter/customized_libxml/cdata.rb +0 -2
- data/lib/moxml/adapter/customized_libxml/comment.rb +0 -2
- data/lib/moxml/adapter/customized_libxml/element.rb +3 -5
- data/lib/moxml/adapter/customized_libxml/node.rb +2 -2
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +0 -2
- data/lib/moxml/adapter/customized_libxml/text.rb +0 -2
- data/lib/moxml/adapter/customized_rexml/formatter.rb +3 -4
- data/lib/moxml/adapter/headed_ox.rb +1 -5
- data/lib/moxml/adapter/libxml.rb +12 -10
- data/lib/moxml/adapter/nokogiri.rb +4 -2
- data/lib/moxml/adapter/oga.rb +10 -10
- data/lib/moxml/adapter/ox.rb +29 -16
- data/lib/moxml/adapter/rexml.rb +5 -6
- data/lib/moxml/adapter.rb +17 -7
- data/lib/moxml/attribute.rb +5 -2
- data/lib/moxml/config.rb +2 -1
- data/lib/moxml/context.rb +8 -8
- data/lib/moxml/declaration.rb +2 -7
- data/lib/moxml/document.rb +0 -11
- data/lib/moxml/document_builder.rb +12 -8
- data/lib/moxml/element.rb +4 -4
- data/lib/moxml/node.rb +37 -14
- data/lib/moxml/node_set.rb +2 -4
- data/lib/moxml/sax/block_handler.rb +0 -2
- data/lib/moxml/sax/element_handler.rb +0 -2
- data/lib/moxml/sax/namespace_splitter.rb +5 -4
- data/lib/moxml/sax.rb +4 -25
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_utils.rb +1 -3
- data/lib/moxml/xpath/compiler.rb +1 -49
- data/lib/moxml/xpath/conversion.rb +7 -6
- data/lib/moxml/xpath/ruby/generator.rb +12 -19
- data/lib/moxml/xpath/ruby/node.rb +1 -9
- data/lib/moxml/xpath.rb +6 -14
- data/lib/moxml.rb +67 -20
- data/moxml.gemspec +1 -1
- data/spec/moxml/adapter/platform_spec.rb +64 -46
- data/spec/moxml/attribute_spec.rb +16 -0
- data/spec/moxml/context_spec.rb +14 -0
- data/spec/moxml/moxml_spec.rb +13 -0
- data/spec/moxml/node_spec.rb +58 -0
- data/spec/moxml/opal_oga_adapter_spec.rb +14 -0
- data/spec/moxml/opal_oga_features_spec.rb +212 -0
- data/spec/moxml/opal_oga_smoke_spec.rb +35 -0
- data/spec/moxml/xpath/functions/node_functions_spec.rb +1 -1
- data/spec/moxml/xpath/ruby/node_spec.rb +3 -3
- data/spec/spec_helper.rb +1 -1
- metadata +7 -2
|
@@ -2,69 +2,87 @@
|
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
|
|
5
|
-
RSpec.describe Moxml::Adapter
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
5
|
+
RSpec.describe Moxml::Adapter do
|
|
6
|
+
describe ".platform_adapters" do
|
|
7
|
+
it "includes all known adapters under MRI" do
|
|
8
|
+
expect(described_class.platform_adapters).to include(:nokogiri, :oga,
|
|
9
|
+
:rexml, :ox)
|
|
10
|
+
end
|
|
20
11
|
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
it "uses the AVAILABLE_ADAPTERS constant under MRI" do
|
|
13
|
+
expect(described_class.platform_adapters).to eq(Moxml::Adapter::AVAILABLE_ADAPTERS)
|
|
14
|
+
end
|
|
23
15
|
end
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.and_return(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
|
|
17
|
+
describe ".available?" do
|
|
18
|
+
it "returns true for :oga" do
|
|
19
|
+
expect(described_class.available?(:oga)).to be true
|
|
29
20
|
end
|
|
30
21
|
|
|
31
|
-
it "returns
|
|
32
|
-
expect(described_class.available?(:nokogiri)).to be
|
|
22
|
+
it "returns true for :nokogiri under MRI" do
|
|
23
|
+
expect(described_class.available?(:nokogiri)).to be true
|
|
33
24
|
end
|
|
34
25
|
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
context "when Opal platform adapters are in effect" do
|
|
27
|
+
before do
|
|
28
|
+
allow(described_class).to receive(:platform_adapters)
|
|
29
|
+
.and_return(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "returns false for :nokogiri" do
|
|
33
|
+
expect(described_class.available?(:nokogiri)).to be false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "returns true for :oga" do
|
|
37
|
+
expect(described_class.available?(:oga)).to be true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "returns true for :rexml" do
|
|
41
|
+
expect(described_class.available?(:rexml)).to be true
|
|
42
|
+
end
|
|
37
43
|
end
|
|
38
44
|
end
|
|
39
|
-
end
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
describe ".load" do
|
|
47
|
+
context "when Opal platform adapters are in effect" do
|
|
48
|
+
before do
|
|
49
|
+
allow(described_class).to receive(:platform_adapters)
|
|
50
|
+
.and_return(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
|
|
51
|
+
end
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
it "raises AdapterError for :nokogiri" do
|
|
54
|
+
expect { described_class.load(:nokogiri) }.to raise_error(
|
|
55
|
+
Moxml::AdapterError, /not available on this platform/
|
|
56
|
+
)
|
|
57
|
+
end
|
|
52
58
|
end
|
|
53
59
|
end
|
|
54
|
-
end
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
describe "OPAL_AVAILABLE_ADAPTERS" do
|
|
62
|
+
it "lists :oga as the primary Opal adapter, with :rexml as opt-in" do
|
|
63
|
+
expect(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS).to eq(%i[oga rexml])
|
|
64
|
+
end
|
|
59
65
|
end
|
|
60
|
-
end
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
describe "CONST_NAME_MAP" do
|
|
68
|
+
it "maps :headed_ox to HeadedOx" do
|
|
69
|
+
expect(Moxml::Adapter::CONST_NAME_MAP[:headed_ox]).to eq("HeadedOx")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "falls back to capitalize for unmapped adapters" do
|
|
73
|
+
expect(Moxml::Adapter::CONST_NAME_MAP[:nokogiri]).to be_nil
|
|
74
|
+
end
|
|
65
75
|
end
|
|
66
76
|
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
describe "default adapter / available list invariants" do
|
|
78
|
+
it "DEFAULT_ADAPTER is a member of AVAILABLE_ADAPTERS" do
|
|
79
|
+
expect(Moxml::Adapter::AVAILABLE_ADAPTERS)
|
|
80
|
+
.to include(Moxml::Config::DEFAULT_ADAPTER)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "OPAL_DEFAULT_ADAPTER is a member of OPAL_AVAILABLE_ADAPTERS" do
|
|
84
|
+
expect(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
|
|
85
|
+
.to include(Moxml::Config::OPAL_DEFAULT_ADAPTER)
|
|
86
|
+
end
|
|
69
87
|
end
|
|
70
88
|
end
|
|
@@ -27,4 +27,20 @@ RSpec.describe Moxml::Attribute do
|
|
|
27
27
|
expect(attr.to_s).to match(/\w+="\w+"/)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
|
+
|
|
31
|
+
describe "#element" do
|
|
32
|
+
it "returns wrapped Element" do
|
|
33
|
+
attr = element.attributes.find { |a| a.name == "id" }
|
|
34
|
+
parent = attr.element
|
|
35
|
+
expect(parent).to be_a(Moxml::Element)
|
|
36
|
+
expect(parent.name).to eq("root")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe "#content" do
|
|
41
|
+
it "returns attribute value" do
|
|
42
|
+
attr = element.attributes.find { |a| a.name == "id" }
|
|
43
|
+
expect(attr.content).to eq("123")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
30
46
|
end
|
data/spec/moxml/context_spec.rb
CHANGED
|
@@ -25,4 +25,18 @@ RSpec.describe Moxml::Context do
|
|
|
25
25
|
expect(context.config.adapter.ancestors).to include(Moxml::Adapter::Base)
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
|
+
|
|
29
|
+
describe "#build" do
|
|
30
|
+
it "creates a document via builder DSL" do
|
|
31
|
+
doc = context.build do
|
|
32
|
+
element("root") do
|
|
33
|
+
element("child") do
|
|
34
|
+
text("hello")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
expect(doc).to be_a(Moxml::Document)
|
|
39
|
+
expect(doc.root.name).to eq("root")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
28
42
|
end
|
data/spec/moxml/moxml_spec.rb
CHANGED
|
@@ -52,4 +52,17 @@ RSpec.describe Moxml do
|
|
|
52
52
|
expect(Moxml::Config::VALID_ADAPTERS).to include(Moxml::Config.runtime_default_adapter)
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
|
+
|
|
56
|
+
describe ".parse" do
|
|
57
|
+
it "parses XML with shorthand" do
|
|
58
|
+
doc = described_class.parse("<root>hello</root>")
|
|
59
|
+
expect(doc).to be_a(Moxml::Document)
|
|
60
|
+
expect(doc.root.text).to eq("hello")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "accepts adapter option" do
|
|
64
|
+
doc = described_class.parse("<root/>", adapter: :rexml)
|
|
65
|
+
expect(doc).to be_a(Moxml::Document)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
55
68
|
end
|
data/spec/moxml/node_spec.rb
CHANGED
|
@@ -34,4 +34,62 @@ RSpec.describe Moxml::Node do
|
|
|
34
34
|
expect(node.children).to be_empty
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
|
+
|
|
38
|
+
describe "#each" do
|
|
39
|
+
it "yields direct children" do
|
|
40
|
+
children = node.map { |c| c }
|
|
41
|
+
expect(children.size).to eq(1)
|
|
42
|
+
expect(children.first).to be_a(Moxml::Element)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "includes Enumerable" do
|
|
46
|
+
expect(node.map(&:name)).to eq(["child"])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe "#outer_xml" do
|
|
51
|
+
it "returns same as to_xml" do
|
|
52
|
+
child = node.children.first
|
|
53
|
+
expect(child.outer_xml).to eq(child.to_xml)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe "#before" do
|
|
58
|
+
it "adds a node before self" do
|
|
59
|
+
child = node.children.first
|
|
60
|
+
child.before("before")
|
|
61
|
+
siblings = node.children.map(&:content).join
|
|
62
|
+
expect(siblings).to include("before")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe "#after" do
|
|
67
|
+
it "adds a node after self" do
|
|
68
|
+
child = node.children.first
|
|
69
|
+
child.after("after")
|
|
70
|
+
expect(node.children.size).to be >= 2
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe "#blank?" do
|
|
75
|
+
it "returns true for whitespace-only text" do
|
|
76
|
+
doc2 = context.parse("<root> </root>")
|
|
77
|
+
expect(doc2.root).to be_blank
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "returns false for non-blank content" do
|
|
81
|
+
expect(node).not_to be_blank
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe "#content" do
|
|
86
|
+
it "returns empty string on base Node" do
|
|
87
|
+
expect(described_class.new(nil, context).content).to eq("")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "returns text on Element" do
|
|
91
|
+
child = node.children.first
|
|
92
|
+
expect(child.content).to eq("text")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
37
95
|
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::Oga, if: RUBY_ENGINE == "opal" do
|
|
7
|
+
around do |example|
|
|
8
|
+
Moxml.with_config(:oga, true, "UTF-8") do
|
|
9
|
+
example.run
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it_behaves_like "xml adapter"
|
|
14
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Exercises feature areas under Opal that are not covered by the shared
|
|
6
|
+
# adapter contract (opal_oga_adapter_spec.rb) or the basic smoke spec
|
|
7
|
+
# (opal_oga_smoke_spec.rb). Targets the default Opal adapter (:oga).
|
|
8
|
+
# Uses an explicit :oga context so leaked global config from other specs
|
|
9
|
+
# cannot flip the adapter under test.
|
|
10
|
+
RSpec.describe "Moxml Opal oga feature coverage", if: RUBY_ENGINE == "opal" do
|
|
11
|
+
let(:context) { Moxml.new(:oga) }
|
|
12
|
+
|
|
13
|
+
describe "builder DSL" do
|
|
14
|
+
it "builds a document with the block DSL" do
|
|
15
|
+
doc = Moxml::Builder.new(context).build do
|
|
16
|
+
root do
|
|
17
|
+
child "text"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
expect(doc.root.name).to eq("root")
|
|
22
|
+
expect(doc.root.children.first.name).to eq("child")
|
|
23
|
+
expect(doc.root.children.first.text).to eq("text")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "creates nested elements via blocks" do
|
|
27
|
+
doc = Moxml::Builder.new(context).build do
|
|
28
|
+
library do
|
|
29
|
+
book(id: "1") { title "A" }
|
|
30
|
+
book(id: "2") { title "B" }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
books = doc.root.children.grep(Moxml::Element)
|
|
35
|
+
expect(books.length).to eq(2)
|
|
36
|
+
expect(books.map { |b| b["id"] }).to eq(%w[1 2])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "supports method_missing DSL with attributes and text" do
|
|
40
|
+
doc = Moxml::Builder.new(context).build do
|
|
41
|
+
person(name: "Alice", age: "30") { email "alice@example.com" }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
person = doc.root
|
|
45
|
+
expect(person.name).to eq("person")
|
|
46
|
+
expect(person["name"]).to eq("Alice")
|
|
47
|
+
expect(person["age"]).to eq("30")
|
|
48
|
+
expect(person.children.first.name).to eq("email")
|
|
49
|
+
expect(person.children.first.text).to eq("alice@example.com")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "strips trailing underscore for reserved-name tags" do
|
|
53
|
+
doc = Moxml::Builder.new(context).build do
|
|
54
|
+
class_ { name "Foo" }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
expect(doc.root.name).to eq("class")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "attaches namespace declarations" do
|
|
61
|
+
builder = Moxml::Builder.new(context)
|
|
62
|
+
doc = builder.build do
|
|
63
|
+
root("xmlns:dc": "http://purl.org/dc/elements/1.1/") do
|
|
64
|
+
builder.element("dc:title") { builder.text "Hello" }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
title = doc.root.children.find { |c| c.is_a?(Moxml::Element) }
|
|
69
|
+
expect(title.name).to eq("dc:title")
|
|
70
|
+
expect(doc.to_xml).to include("xmlns:dc")
|
|
71
|
+
expect(doc.to_xml).to include("dc:title")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe "XML declaration preservation" do
|
|
76
|
+
it "does not add a declaration when the input has none" do
|
|
77
|
+
doc = context.parse("<root><child/></root>")
|
|
78
|
+
output = doc.to_xml
|
|
79
|
+
|
|
80
|
+
expect(output).not_to include("<?xml")
|
|
81
|
+
expect(doc.has_xml_declaration).to be false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "preserves the declaration when the input has one" do
|
|
85
|
+
xml = '<?xml version="1.0" encoding="UTF-8"?><root/>'
|
|
86
|
+
doc = context.parse(xml)
|
|
87
|
+
|
|
88
|
+
expect(doc.has_xml_declaration).to be true
|
|
89
|
+
expect(doc.to_xml).to include("<?xml")
|
|
90
|
+
expect(doc.to_xml).to include('version="1.0"')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "forces the declaration on with declaration: true" do
|
|
94
|
+
doc = context.parse("<root/>")
|
|
95
|
+
expect(doc.to_xml(declaration: true)).to include("<?xml")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "forces the declaration off with declaration: false" do
|
|
99
|
+
doc = context.parse('<?xml version="1.0"?><root/>')
|
|
100
|
+
expect(doc.to_xml(declaration: false)).not_to include("<?xml")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "preserves standalone attribute" do
|
|
104
|
+
xml = '<?xml version="1.0" standalone="yes"?><root/>'
|
|
105
|
+
doc = context.parse(xml)
|
|
106
|
+
expect(doc.to_xml).to include("standalone")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe "doctype handling" do
|
|
111
|
+
it "parses a SIMPLE doctype" do
|
|
112
|
+
doc = context.parse("<!DOCTYPE root><root/>")
|
|
113
|
+
doctype = doc.children.find { |c| c.is_a?(Moxml::Doctype) }
|
|
114
|
+
expect(doctype).not_to be_nil
|
|
115
|
+
expect(doctype.name).to eq("root")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "parses a PUBLIC doctype with external and system ids" do
|
|
119
|
+
xml = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><html/>'
|
|
120
|
+
doc = context.parse(xml)
|
|
121
|
+
doctype = doc.children.find { |c| c.is_a?(Moxml::Doctype) }
|
|
122
|
+
|
|
123
|
+
expect(doctype).not_to be_nil
|
|
124
|
+
expect(doctype.name).to eq("html")
|
|
125
|
+
expect(doctype.external_id).to eq("-//W3C//DTD HTML 4.01//EN")
|
|
126
|
+
expect(doctype.system_id).to eq("http://www.w3.org/TR/html4/strict.dtd")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "round-trips the doctype through serialization" do
|
|
130
|
+
xml = "<!DOCTYPE root><root/>"
|
|
131
|
+
doc = context.parse(xml)
|
|
132
|
+
expect(doc.to_xml).to include("DOCTYPE")
|
|
133
|
+
expect(doc.to_xml).to include("root")
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe "round-trip stability" do
|
|
138
|
+
it "round-trips a document with mixed content" do
|
|
139
|
+
xml = <<~XML.strip
|
|
140
|
+
<root attr="value">
|
|
141
|
+
<child>text</child>
|
|
142
|
+
<!-- comment -->
|
|
143
|
+
<nested><deep>data</deep></nested>
|
|
144
|
+
</root>
|
|
145
|
+
XML
|
|
146
|
+
|
|
147
|
+
doc1 = context.parse(xml)
|
|
148
|
+
serialized1 = doc1.to_xml
|
|
149
|
+
doc2 = context.parse(serialized1)
|
|
150
|
+
serialized2 = doc2.to_xml
|
|
151
|
+
|
|
152
|
+
expect(serialized2).to eq(serialized1)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "round-trips a document with namespaces" do
|
|
156
|
+
xml = '<root xmlns:ns="http://example.com/ns"><ns:child>content</ns:child></root>'
|
|
157
|
+
doc1 = context.parse(xml)
|
|
158
|
+
serialized1 = doc1.to_xml
|
|
159
|
+
|
|
160
|
+
doc2 = context.parse(serialized1)
|
|
161
|
+
child = doc2.root.children.find { |c| c.is_a?(Moxml::Element) }
|
|
162
|
+
expect(child.name).to eq("child")
|
|
163
|
+
expect(child.namespace_prefix).to eq("ns")
|
|
164
|
+
expect(doc2.to_xml).to include("xmlns:ns")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it "round-trips a document with CDATA" do
|
|
168
|
+
xml = "<root><![CDATA[<unparsed>content</unparsed>]]></root>"
|
|
169
|
+
doc1 = context.parse(xml)
|
|
170
|
+
serialized1 = doc1.to_xml
|
|
171
|
+
|
|
172
|
+
expect(serialized1).to include("<![CDATA[")
|
|
173
|
+
doc2 = context.parse(serialized1)
|
|
174
|
+
expect(doc2.to_xml).to include("<![CDATA[")
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
describe "XPath under Opal" do
|
|
179
|
+
it "evaluates element-only XPath" do
|
|
180
|
+
doc = context.parse("<root><a><b>1</b></a><a><b>2</b></a></root>")
|
|
181
|
+
results = doc.xpath("//a/b")
|
|
182
|
+
expect(results.length).to eq(2)
|
|
183
|
+
expect(results.map(&:text)).to eq(%w[1 2])
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "supports attribute predicates" do
|
|
187
|
+
doc = context.parse('<root><item id="x">1</item><item id="y">2</item></root>')
|
|
188
|
+
found = doc.xpath("//item").find { |i| i["id"] == "y" }
|
|
189
|
+
expect(found.text).to eq("2")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "supports at_xpath" do
|
|
193
|
+
doc = context.parse("<root><a>1</a><a>2</a></root>")
|
|
194
|
+
first = doc.at_xpath("//a")
|
|
195
|
+
expect(first.text).to eq("1")
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
describe "comment and processing instruction handling" do
|
|
200
|
+
it "preserves comments through parse/serialize" do
|
|
201
|
+
xml = "<root><!-- my comment --></root>"
|
|
202
|
+
doc = context.parse(xml)
|
|
203
|
+
expect(doc.to_xml).to include("my comment")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it "preserves processing instructions through parse/serialize" do
|
|
207
|
+
xml = "<?pi-target pi-data?><root/>"
|
|
208
|
+
doc = context.parse(xml)
|
|
209
|
+
expect(doc.to_xml).to include("pi-target")
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Verifies that the Opal default adapter (:oga, set in Moxml::Config)
|
|
6
|
+
# loads correctly via the Opal-compatible oga fork. The fork's
|
|
7
|
+
# lib/oga.rb dispatches `require 'liboga'` to `require 'oga/native/lexer'`
|
|
8
|
+
# under Opal, which resolves to the pure-Ruby lexer vendored at
|
|
9
|
+
# vendor/opal-oga/ext/pureruby/.
|
|
10
|
+
RSpec.describe "Moxml Opal oga default", if: RUBY_ENGINE == "opal" do
|
|
11
|
+
let(:context) { Moxml.new }
|
|
12
|
+
|
|
13
|
+
it "defaults to :oga under Opal" do
|
|
14
|
+
expect(Moxml::Config::OPAL_DEFAULT_ADAPTER).to eq(:oga)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "parses XML through the oga adapter" do
|
|
18
|
+
doc = context.parse("<root><child>text</child></root>")
|
|
19
|
+
expect(doc.root.name).to eq("root")
|
|
20
|
+
expect(doc.root.children.first.name).to eq("child")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "decodes entities single-pass" do
|
|
24
|
+
doc = context.parse("<root>&#38;</root>")
|
|
25
|
+
expect(doc.root.text).to eq("&")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "serializes back to XML" do
|
|
29
|
+
xml = '<person name="Alice"><age>30</age></person>'
|
|
30
|
+
doc = context.parse(xml)
|
|
31
|
+
serialized = doc.to_xml
|
|
32
|
+
expect(serialized).to include("person")
|
|
33
|
+
expect(serialized).to include("Alice")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -113,13 +113,13 @@ RSpec.describe Moxml::XPath::Ruby::Node do
|
|
|
113
113
|
describe "#is_a?" do
|
|
114
114
|
it "creates a :send node for is_a? method call" do
|
|
115
115
|
node = described_class.new(:lit, ["obj"])
|
|
116
|
-
|
|
116
|
+
klass_node = described_class.new(:const, ["String"])
|
|
117
|
+
result = node.is_a?(klass_node)
|
|
117
118
|
|
|
118
119
|
expect(result.type).to eq(:send)
|
|
119
120
|
expect(result.to_a[0]).to eq(node)
|
|
120
121
|
expect(result.to_a[1]).to eq("is_a?")
|
|
121
|
-
expect(result.to_a[2]
|
|
122
|
-
expect(result.to_a[2].to_a[0]).to eq("String")
|
|
122
|
+
expect(result.to_a[2]).to eq(klass_node)
|
|
123
123
|
end
|
|
124
124
|
end
|
|
125
125
|
|
data/spec/spec_helper.rb
CHANGED
|
@@ -101,7 +101,7 @@ RSpec.configure do |config|
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
Moxml.configure do |config|
|
|
104
|
-
config.adapter = RUBY_ENGINE == "opal" ?
|
|
104
|
+
config.adapter = RUBY_ENGINE == "opal" ? Moxml::Config::OPAL_DEFAULT_ADAPTER : Moxml::Config::DEFAULT_ADAPTER
|
|
105
105
|
config.strict_parsing = true
|
|
106
106
|
config.default_encoding = "UTF-8"
|
|
107
107
|
config.entity_load_mode = :optional if RUBY_ENGINE == "opal"
|
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.25
|
|
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-06-
|
|
11
|
+
date: 2026-06-26 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
|
|
@@ -28,6 +28,7 @@ files:
|
|
|
28
28
|
- ".github/workflows/release.yml"
|
|
29
29
|
- ".github/workflows/round-trip.yml"
|
|
30
30
|
- ".gitignore"
|
|
31
|
+
- ".gitmodules"
|
|
31
32
|
- ".rspec"
|
|
32
33
|
- ".rspec-opal"
|
|
33
34
|
- ".rubocop.yml"
|
|
@@ -98,6 +99,7 @@ files:
|
|
|
98
99
|
- examples/web_scraper/README.md
|
|
99
100
|
- examples/web_scraper/example_page.html
|
|
100
101
|
- examples/web_scraper/web_scraper.rb
|
|
102
|
+
- lib/compat/opal/moxml_boot.rb
|
|
101
103
|
- lib/compat/opal/rexml/namespace.rb
|
|
102
104
|
- lib/compat/opal/rexml/parsers/baseparser.rb
|
|
103
105
|
- lib/compat/opal/rexml/source.rb
|
|
@@ -351,6 +353,9 @@ files:
|
|
|
351
353
|
- spec/moxml/node_set_spec.rb
|
|
352
354
|
- spec/moxml/node_spec.rb
|
|
353
355
|
- spec/moxml/node_type_map_spec.rb
|
|
356
|
+
- spec/moxml/opal_oga_adapter_spec.rb
|
|
357
|
+
- spec/moxml/opal_oga_features_spec.rb
|
|
358
|
+
- spec/moxml/opal_oga_smoke_spec.rb
|
|
354
359
|
- spec/moxml/opal_rexml_adapter_spec.rb
|
|
355
360
|
- spec/moxml/opal_smoke_spec.rb
|
|
356
361
|
- spec/moxml/processing_instruction_spec.rb
|