moxml 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/rake.yml +15 -0
- data/.github/workflows/release.yml +23 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +65 -0
- data/.ruby-version +1 -0
- data/Gemfile +10 -3
- data/README.adoc +400 -594
- data/lib/moxml/adapter/base.rb +102 -0
- data/lib/moxml/adapter/customized_oga/xml_declaration.rb +18 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +104 -0
- data/lib/moxml/adapter/nokogiri.rb +314 -0
- data/lib/moxml/adapter/oga.rb +309 -0
- data/lib/moxml/adapter/ox.rb +325 -0
- data/lib/moxml/adapter.rb +26 -170
- data/lib/moxml/attribute.rb +47 -14
- data/lib/moxml/builder.rb +64 -0
- data/lib/moxml/cdata.rb +4 -26
- data/lib/moxml/comment.rb +6 -22
- data/lib/moxml/config.rb +39 -15
- data/lib/moxml/context.rb +29 -0
- data/lib/moxml/declaration.rb +16 -26
- data/lib/moxml/doctype.rb +9 -0
- data/lib/moxml/document.rb +51 -63
- data/lib/moxml/document_builder.rb +87 -0
- data/lib/moxml/element.rb +61 -99
- data/lib/moxml/error.rb +20 -0
- data/lib/moxml/namespace.rb +12 -37
- data/lib/moxml/node.rb +78 -58
- data/lib/moxml/node_set.rb +19 -222
- data/lib/moxml/processing_instruction.rb +6 -25
- data/lib/moxml/text.rb +4 -26
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_utils/encoder.rb +55 -0
- data/lib/moxml/xml_utils.rb +80 -0
- data/lib/moxml.rb +33 -33
- data/moxml.gemspec +1 -1
- data/spec/moxml/adapter/nokogiri_spec.rb +14 -0
- data/spec/moxml/adapter/oga_spec.rb +14 -0
- data/spec/moxml/adapter/ox_spec.rb +49 -0
- data/spec/moxml/all_with_adapters_spec.rb +46 -0
- data/spec/moxml/config_spec.rb +55 -0
- data/spec/moxml/error_spec.rb +71 -0
- data/spec/moxml/examples/adapter_spec.rb +27 -0
- data/spec/moxml_spec.rb +50 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/shared_examples/attribute.rb +165 -0
- data/spec/support/shared_examples/builder.rb +25 -0
- data/spec/support/shared_examples/cdata.rb +70 -0
- data/spec/support/shared_examples/comment.rb +65 -0
- data/spec/support/shared_examples/context.rb +35 -0
- data/spec/support/shared_examples/declaration.rb +93 -0
- data/spec/support/shared_examples/doctype.rb +25 -0
- data/spec/support/shared_examples/document.rb +110 -0
- data/spec/support/shared_examples/document_builder.rb +43 -0
- data/spec/support/shared_examples/edge_cases.rb +185 -0
- data/spec/support/shared_examples/element.rb +110 -0
- data/spec/support/shared_examples/examples/attribute.rb +42 -0
- data/spec/support/shared_examples/examples/basic_usage.rb +67 -0
- data/spec/support/shared_examples/examples/memory.rb +54 -0
- data/spec/support/shared_examples/examples/namespace.rb +65 -0
- data/spec/support/shared_examples/examples/readme_examples.rb +100 -0
- data/spec/support/shared_examples/examples/thread_safety.rb +43 -0
- data/spec/support/shared_examples/examples/xpath.rb +39 -0
- data/spec/support/shared_examples/integration.rb +135 -0
- data/spec/support/shared_examples/namespace.rb +96 -0
- data/spec/support/shared_examples/node.rb +110 -0
- data/spec/support/shared_examples/node_set.rb +90 -0
- data/spec/support/shared_examples/processing_instruction.rb +88 -0
- data/spec/support/shared_examples/text.rb +66 -0
- data/spec/support/shared_examples/xml_adapter.rb +191 -0
- data/spec/support/xml_matchers.rb +27 -0
- metadata +55 -6
- data/.github/workflows/main.yml +0 -27
- data/lib/moxml/error_handler.rb +0 -77
- data/lib/moxml/errors.rb +0 -169
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.shared_examples "Moxml::Attribute" do
|
4
|
+
describe Moxml::Attribute do
|
5
|
+
let(:context) { Moxml.new }
|
6
|
+
let(:doc) { context.create_document }
|
7
|
+
let(:element) { doc.create_element("test") }
|
8
|
+
|
9
|
+
describe "creation and access" do
|
10
|
+
before do
|
11
|
+
element["test"] = "value"
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:attribute) { element.attributes.first }
|
15
|
+
|
16
|
+
it "creates attribute" do
|
17
|
+
expect(attribute).to be_a(described_class)
|
18
|
+
expect(attribute).to be_attribute
|
19
|
+
end
|
20
|
+
|
21
|
+
it "gets name" do
|
22
|
+
expect(attribute.name).to eq("test")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "gets value" do
|
26
|
+
expect(attribute.value).to eq("value")
|
27
|
+
end
|
28
|
+
|
29
|
+
it "sets name" do
|
30
|
+
attribute.name = "new_name"
|
31
|
+
expect(attribute.name).to eq("new_name")
|
32
|
+
expect(element["new_name"]).to eq("value")
|
33
|
+
expect(element["test"]).to be_nil
|
34
|
+
end
|
35
|
+
|
36
|
+
it "sets value" do
|
37
|
+
attribute.value = "new_value"
|
38
|
+
expect(attribute.value).to eq("new_value")
|
39
|
+
expect(element["test"]).to eq("new_value")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "namespaces" do
|
44
|
+
before do
|
45
|
+
element.add_namespace("ns", "http://example.org")
|
46
|
+
end
|
47
|
+
|
48
|
+
it "handles namespaced attributes" do
|
49
|
+
element["ns:attr"] = "value"
|
50
|
+
attribute = element.attributes.first
|
51
|
+
|
52
|
+
expect(attribute.namespace).not_to be_nil
|
53
|
+
expect(attribute.namespace.prefix).to eq("ns")
|
54
|
+
expect(attribute.namespace.uri).to eq("http://example.org")
|
55
|
+
end
|
56
|
+
|
57
|
+
it "changes attribute namespace" do
|
58
|
+
element["attr"] = "value"
|
59
|
+
attribute = element.attributes.first
|
60
|
+
|
61
|
+
element.add_namespace("other", "http://other.org")
|
62
|
+
new_ns = element.namespaces.last
|
63
|
+
attribute.namespace = new_ns
|
64
|
+
|
65
|
+
expect(attribute.namespace.prefix).to eq("other")
|
66
|
+
expect(attribute.to_s).to eq('other:attr="value"')
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "manipulation" do
|
71
|
+
before do
|
72
|
+
element["test"] = "value"
|
73
|
+
end
|
74
|
+
|
75
|
+
let(:attribute) do
|
76
|
+
element.attributes.first
|
77
|
+
end
|
78
|
+
|
79
|
+
it "removes attribute" do
|
80
|
+
attribute.remove
|
81
|
+
expect(element["test"]).to be_nil
|
82
|
+
expect(element.attributes).to be_empty
|
83
|
+
end
|
84
|
+
|
85
|
+
it "handles special characters in values" do
|
86
|
+
attribute.value = "< & > ' \""
|
87
|
+
expect(attribute.to_s).to eq('test="< & > \' ""')
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#to_s" do
|
92
|
+
it "formats regular attribute" do
|
93
|
+
element["test"] = "value"
|
94
|
+
expect(element.attributes.first.to_s).to eq('test="value"')
|
95
|
+
end
|
96
|
+
|
97
|
+
it "formats namespaced attribute" do
|
98
|
+
element.add_namespace("ns", "http://example.org")
|
99
|
+
element["ns:test"] = "value"
|
100
|
+
expect(element.attributes.first.to_s).to eq('ns:test="value"')
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "equality" do
|
105
|
+
it "compares attributes" do
|
106
|
+
element["test"] = "value"
|
107
|
+
attr1 = element.attributes.first
|
108
|
+
|
109
|
+
element2 = doc.create_element("other")
|
110
|
+
element2["test"] = "value"
|
111
|
+
attr2 = element2.attributes.first
|
112
|
+
|
113
|
+
element3 = doc.create_element("other")
|
114
|
+
element3["test"] = "different"
|
115
|
+
attr3 = element3.attributes.first
|
116
|
+
|
117
|
+
expect(attr1).to eq(attr2)
|
118
|
+
expect(attr1).not_to eq(attr3)
|
119
|
+
end
|
120
|
+
|
121
|
+
it "compares attributes with namespaces" do
|
122
|
+
element.add_namespace("ns", "http://example.org")
|
123
|
+
element["ns:test"] = "value"
|
124
|
+
attr1 = element.attributes.first
|
125
|
+
|
126
|
+
element2 = doc.create_element("other")
|
127
|
+
element2.add_namespace("ns", "http://example.org")
|
128
|
+
element2["ns:test"] = "value"
|
129
|
+
attr2 = element2.attributes.first
|
130
|
+
|
131
|
+
expect(attr1).to eq(attr2)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe "edge cases" do
|
136
|
+
it "handles empty values" do
|
137
|
+
element["empty"] = ""
|
138
|
+
attribute = element.attributes.first
|
139
|
+
expect(attribute.value).to eq("")
|
140
|
+
expect(attribute.to_s).to eq('empty=""')
|
141
|
+
end
|
142
|
+
|
143
|
+
it "handles nil values" do
|
144
|
+
element["nil"] = nil
|
145
|
+
attribute = element.attributes.first
|
146
|
+
expect(attribute.value).to eq("")
|
147
|
+
expect(attribute.to_s).to eq('nil=""')
|
148
|
+
end
|
149
|
+
|
150
|
+
it "handles numeric values" do
|
151
|
+
element["num"] = 123
|
152
|
+
attribute = element.attributes.first
|
153
|
+
expect(attribute.value).to eq("123")
|
154
|
+
expect(attribute.to_s).to eq('num="123"')
|
155
|
+
end
|
156
|
+
|
157
|
+
it "handles boolean values" do
|
158
|
+
element["bool"] = true
|
159
|
+
attribute = element.attributes.first
|
160
|
+
expect(attribute.value).to eq("true")
|
161
|
+
expect(attribute.to_s).to eq('bool="true"')
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.shared_examples "Moxml::Builder" do
|
4
|
+
let(:context) { Moxml.new }
|
5
|
+
let(:builder) { Moxml::Builder.new(context) }
|
6
|
+
|
7
|
+
describe "#document" do
|
8
|
+
it "creates a well-formed document" do
|
9
|
+
doc = builder.build do
|
10
|
+
declaration version: "1.0", encoding: "UTF-8"
|
11
|
+
element "root" do
|
12
|
+
element "child", id: "1" do
|
13
|
+
text "content"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
xml = doc.to_xml
|
19
|
+
expect(xml).to include('<?xml version="1.0" encoding="UTF-8"?>')
|
20
|
+
expect(xml).to include("<root>")
|
21
|
+
expect(xml).to include('<child id="1">content</child>')
|
22
|
+
expect(xml).to include("</root>")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.shared_examples "Moxml::Cdata" do
|
4
|
+
let(:context) { Moxml.new }
|
5
|
+
let(:doc) { context.create_document }
|
6
|
+
let(:cdata) { doc.create_cdata("<content>") }
|
7
|
+
|
8
|
+
it "identifies as CDATA node" do
|
9
|
+
expect(cdata).to be_cdata
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "content manipulation" do
|
13
|
+
it "gets content" do
|
14
|
+
expect(cdata.content).to eq("<content>")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "sets content" do
|
18
|
+
cdata.content = "new <content>"
|
19
|
+
expect(cdata.content).to eq("new <content>")
|
20
|
+
end
|
21
|
+
|
22
|
+
it "handles nil content" do
|
23
|
+
cdata.content = nil
|
24
|
+
expect(cdata.content).to eq("")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "serialization" do
|
29
|
+
it "wraps content in CDATA section" do
|
30
|
+
expect(cdata.to_xml).to eq("<![CDATA[<content>]]>")
|
31
|
+
end
|
32
|
+
|
33
|
+
it "escapes CDATA end marker" do
|
34
|
+
cdata.content = "content]]>more"
|
35
|
+
expect(cdata.to_xml).to eq("<![CDATA[content]]]]><![CDATA[>more]]>")
|
36
|
+
end
|
37
|
+
|
38
|
+
it "handles special characters" do
|
39
|
+
cdata.content = "< > & \" '"
|
40
|
+
expect(cdata.to_xml).to include("< > & \" '")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "preserves whitespace" do
|
44
|
+
cdata.content = " spaced content\n\t"
|
45
|
+
expect(cdata.content).to eq(" spaced content\n\t")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "node operations" do
|
50
|
+
let(:element) { doc.create_element("test") }
|
51
|
+
|
52
|
+
it "adds to element" do
|
53
|
+
element.add_child(cdata)
|
54
|
+
expect(element.to_xml).to include("<![CDATA[<content>]]>")
|
55
|
+
end
|
56
|
+
|
57
|
+
it "removes from element" do
|
58
|
+
element.add_child(cdata)
|
59
|
+
cdata.remove
|
60
|
+
expect(element.children).to be_empty
|
61
|
+
end
|
62
|
+
|
63
|
+
it "replaces with another node" do
|
64
|
+
element.add_child(cdata)
|
65
|
+
text = doc.create_text("replacement")
|
66
|
+
cdata.replace(text)
|
67
|
+
expect(element.text).to eq("replacement")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.shared_examples "Moxml::Comment" do
|
4
|
+
let(:context) { Moxml.new }
|
5
|
+
let(:doc) { context.create_document }
|
6
|
+
let(:comment) { doc.create_comment("test comment") }
|
7
|
+
|
8
|
+
it "identifies as comment node" do
|
9
|
+
expect(comment).to be_comment
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "content manipulation" do
|
13
|
+
it "gets content" do
|
14
|
+
expect(comment.content).to eq("test comment")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "sets content" do
|
18
|
+
comment.content = "new comment"
|
19
|
+
expect(comment.content).to eq("new comment")
|
20
|
+
end
|
21
|
+
|
22
|
+
it "handles nil content" do
|
23
|
+
comment.content = nil
|
24
|
+
expect(comment.content).to eq("")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "serialization" do
|
29
|
+
it "wraps content in comment markers" do
|
30
|
+
expect(comment.to_xml).to eq("<!--test comment-->")
|
31
|
+
end
|
32
|
+
|
33
|
+
it "raises an error on double hyphens" do
|
34
|
+
expect { comment.content = "test -- comment" }
|
35
|
+
.to raise_error(Moxml::ValidationError, "XML comment cannot contain double hyphens (--)")
|
36
|
+
end
|
37
|
+
|
38
|
+
it "handles special characters" do
|
39
|
+
comment.content = "< > & \" '"
|
40
|
+
expect(comment.to_xml).to eq("<!--< > & \" '-->")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "node operations" do
|
45
|
+
let(:element) { doc.create_element("test") }
|
46
|
+
|
47
|
+
it "adds to element" do
|
48
|
+
element.add_child(comment)
|
49
|
+
expect(element.to_xml).to include("<!--test comment-->")
|
50
|
+
end
|
51
|
+
|
52
|
+
it "removes from element" do
|
53
|
+
element.add_child(comment)
|
54
|
+
comment.remove
|
55
|
+
expect(element.children).to be_empty
|
56
|
+
end
|
57
|
+
|
58
|
+
it "replaces with another node" do
|
59
|
+
element.add_child(comment)
|
60
|
+
text = doc.create_text("replacement")
|
61
|
+
comment.replace(text)
|
62
|
+
expect(element.text).to eq("replacement")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.shared_examples "Moxml::Context" do
|
4
|
+
let(:context) { Moxml::Context.new }
|
5
|
+
|
6
|
+
describe "#parse" do
|
7
|
+
it "returns a Moxml::Document" do
|
8
|
+
doc = context.parse("<root/>")
|
9
|
+
expect(doc).to be_a(Moxml::Document)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "builds complete document model" do
|
13
|
+
doc = context.parse("<root><child>text</child></root>")
|
14
|
+
expect(doc.root).to be_a(Moxml::Element)
|
15
|
+
expect(doc.root.children.first).to be_a(Moxml::Element)
|
16
|
+
expect(doc.root.children.first.children.first).to be_a(Moxml::Text)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "maintains document structure", skip: "Nokogiri doesn't consider the declaration as a child node" do
|
20
|
+
xml = <<~XML
|
21
|
+
<?xml version="1.0"?>
|
22
|
+
<!-- comment -->
|
23
|
+
<root>
|
24
|
+
<![CDATA[data]]>
|
25
|
+
<?pi target?>
|
26
|
+
</root>
|
27
|
+
XML
|
28
|
+
doc = context.parse(xml)
|
29
|
+
|
30
|
+
expect(doc.children[1]).to be_a(Moxml::Comment)
|
31
|
+
expect(doc.at_xpath("//root").children[0]).to be_a(Moxml::Cdata)
|
32
|
+
expect(doc.at_xpath("//root").children[1]).to be_a(Moxml::ProcessingInstruction)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.shared_examples "Moxml::Declaration" do
|
4
|
+
let(:context) { Moxml.new }
|
5
|
+
let(:doc) { context.create_document }
|
6
|
+
let(:declaration) { doc.create_declaration("1.0", "UTF-8", "yes") }
|
7
|
+
|
8
|
+
it "identifies as declaration node" do
|
9
|
+
expect(declaration).to be_declaration
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "version handling" do
|
13
|
+
it "gets version" do
|
14
|
+
expect(declaration.version).to eq("1.0")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "sets version" do
|
18
|
+
declaration.version = "1.1"
|
19
|
+
expect(declaration.version).to eq("1.1")
|
20
|
+
end
|
21
|
+
|
22
|
+
it "validates version" do
|
23
|
+
expect { declaration.version = "2.0" }
|
24
|
+
.to raise_error(Moxml::ValidationError, "Invalid XML version: 2.0")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "encoding handling" do
|
29
|
+
it "gets encoding" do
|
30
|
+
expect(declaration.encoding).to eq("UTF-8")
|
31
|
+
end
|
32
|
+
|
33
|
+
it "sets encoding" do
|
34
|
+
declaration.encoding = "ISO-8859-1"
|
35
|
+
expect(declaration.encoding).to eq("ISO-8859-1")
|
36
|
+
end
|
37
|
+
|
38
|
+
it "normalizes encoding" do
|
39
|
+
declaration.encoding = "utf-8"
|
40
|
+
expect(declaration.encoding).to eq("utf-8")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "standalone handling" do
|
45
|
+
it "gets standalone" do
|
46
|
+
expect(declaration.standalone).to eq("yes")
|
47
|
+
end
|
48
|
+
|
49
|
+
it "sets standalone" do
|
50
|
+
declaration.standalone = "no"
|
51
|
+
expect(declaration.standalone).to eq("no")
|
52
|
+
end
|
53
|
+
|
54
|
+
it "validates standalone value" do
|
55
|
+
expect { declaration.standalone = "maybe" }
|
56
|
+
.to raise_error(Moxml::ValidationError, "Invalid standalone value: maybe")
|
57
|
+
end
|
58
|
+
|
59
|
+
it "allows nil standalone" do
|
60
|
+
declaration.standalone = nil
|
61
|
+
expect(declaration.standalone).to be_nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "serialization" do
|
66
|
+
it "formats complete declaration" do
|
67
|
+
expect(declaration.to_xml).to eq('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>')
|
68
|
+
end
|
69
|
+
|
70
|
+
it "formats minimal declaration with empty encoding" do
|
71
|
+
decl = doc.create_declaration("1.0", nil)
|
72
|
+
expect(decl.to_xml).to eq('<?xml version="1.0"?>')
|
73
|
+
end
|
74
|
+
|
75
|
+
it "formats declaration with encoding only" do
|
76
|
+
decl = doc.create_declaration("1.0", "UTF-8")
|
77
|
+
expect(decl.to_xml).to eq('<?xml version="1.0" encoding="UTF-8"?>')
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "node operations" do
|
82
|
+
it "adds to document" do
|
83
|
+
doc.add_child(declaration)
|
84
|
+
expect(doc.to_xml).to start_with("<?xml")
|
85
|
+
end
|
86
|
+
|
87
|
+
it "removes from document", skip: "The document contains a default declaration" do
|
88
|
+
doc.add_child(declaration)
|
89
|
+
declaration.remove
|
90
|
+
expect(doc.to_xml).not_to include("<?xml")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.shared_examples "Moxml::Doctype" do
|
4
|
+
let(:context) { Moxml.new }
|
5
|
+
let(:doc) { context.create_document }
|
6
|
+
let(:doctype) do
|
7
|
+
doc.create_doctype(
|
8
|
+
"html",
|
9
|
+
"-//W3C//DTD HTML 4.01 Transitional//EN",
|
10
|
+
"http://www.w3.org/TR/html4/loose.dtd"
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "identifies as doctype node" do
|
15
|
+
expect(doctype).to be_doctype
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "serialization" do
|
19
|
+
it "wraps content in doctype markers" do
|
20
|
+
expect(doctype.to_xml).to eq(
|
21
|
+
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.shared_examples "Moxml::Document" do
|
4
|
+
let(:context) { Moxml.new }
|
5
|
+
let(:xml) do
|
6
|
+
<<~XML
|
7
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
8
|
+
<!DOCTYPE html>
|
9
|
+
<root xmlns="http://example.org" xmlns:x="http://example.org/x">
|
10
|
+
<child id="1">Text</child>
|
11
|
+
<child id="2"/>
|
12
|
+
<x:special>
|
13
|
+
<![CDATA[Some <special> text]]>
|
14
|
+
<!-- A comment -->
|
15
|
+
<?pi target?>
|
16
|
+
</x:special>
|
17
|
+
</root>
|
18
|
+
XML
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#parse" do
|
22
|
+
let(:doc) { context.parse(xml) }
|
23
|
+
|
24
|
+
it "parses XML string" do
|
25
|
+
expect(doc).to be_a(Moxml::Document)
|
26
|
+
expect(doc.root.name).to eq("root")
|
27
|
+
end
|
28
|
+
|
29
|
+
it "preserves XML declaration" do
|
30
|
+
expect(doc.to_xml).to include('<?xml version="1.0" encoding="UTF-8"?>')
|
31
|
+
end
|
32
|
+
|
33
|
+
it "handles namespaces" do
|
34
|
+
expect(doc.root.namespaces.size).to eq(2)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "preserves DOCTYPE" do
|
38
|
+
expect(doc.to_xml).to include("<!DOCTYPE html>")
|
39
|
+
end
|
40
|
+
|
41
|
+
it "raises error for invalid XML when strict" do
|
42
|
+
context.config.strict_parsing = true
|
43
|
+
expect { context.parse("<invalid>") }.to raise_error(Moxml::ParseError)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "node creation" do
|
48
|
+
let(:doc) { context.create_document }
|
49
|
+
|
50
|
+
it "creates element" do
|
51
|
+
element = doc.create_element("test")
|
52
|
+
expect(element).to be_a(Moxml::Element)
|
53
|
+
expect(element.name).to eq("test")
|
54
|
+
end
|
55
|
+
|
56
|
+
it "creates text" do
|
57
|
+
text = doc.create_text("content")
|
58
|
+
expect(text).to be_a(Moxml::Text)
|
59
|
+
expect(text.content).to eq("content")
|
60
|
+
end
|
61
|
+
|
62
|
+
it "creates CDATA" do
|
63
|
+
cdata = doc.create_cdata("<content>")
|
64
|
+
expect(cdata).to be_a(Moxml::Cdata)
|
65
|
+
expect(cdata.content).to eq("<content>")
|
66
|
+
end
|
67
|
+
|
68
|
+
it "creates comment" do
|
69
|
+
comment = doc.create_comment("comment")
|
70
|
+
expect(comment).to be_a(Moxml::Comment)
|
71
|
+
expect(comment.content).to eq("comment")
|
72
|
+
end
|
73
|
+
|
74
|
+
it "creates processing instruction" do
|
75
|
+
pi = doc.create_processing_instruction("target", "content")
|
76
|
+
expect(pi).to be_a(Moxml::ProcessingInstruction)
|
77
|
+
expect(pi.target).to eq("target")
|
78
|
+
expect(pi.content).to eq("content")
|
79
|
+
end
|
80
|
+
|
81
|
+
it "creates declaration" do
|
82
|
+
decl = doc.create_declaration("1.0", "UTF-8", "yes")
|
83
|
+
expect(decl).to be_a(Moxml::Declaration)
|
84
|
+
expect(decl.version).to eq("1.0")
|
85
|
+
expect(decl.encoding).to eq("UTF-8")
|
86
|
+
expect(decl.standalone).to eq("yes")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "document structure" do
|
91
|
+
let(:doc) { context.create_document }
|
92
|
+
|
93
|
+
it "builds complete document" do
|
94
|
+
doc.add_child(doc.create_declaration("1.0", "UTF-8"))
|
95
|
+
root = doc.create_element("root")
|
96
|
+
doc.add_child(root)
|
97
|
+
|
98
|
+
child = doc.create_element("child")
|
99
|
+
child.add_child(doc.create_text("text"))
|
100
|
+
root.add_child(child)
|
101
|
+
|
102
|
+
expect(doc.to_xml).to include(
|
103
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
104
|
+
"<root>",
|
105
|
+
"<child>text</child>",
|
106
|
+
"</root>"
|
107
|
+
)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.shared_examples "Moxml::DocumentBuilder" do
|
4
|
+
let(:context) { Moxml.new }
|
5
|
+
let(:builder) { Moxml::DocumentBuilder.new(context) }
|
6
|
+
|
7
|
+
describe "#build" do
|
8
|
+
it "builds a document model from native document" do
|
9
|
+
xml = "<root><child>text</child></root>"
|
10
|
+
doc = context.config.adapter.parse(xml)
|
11
|
+
|
12
|
+
expect(doc).to be_a(Moxml::Document)
|
13
|
+
expect(doc.root).to be_a(Moxml::Element)
|
14
|
+
expect(doc.root.name).to eq("root")
|
15
|
+
expect(doc.root.children.first).to be_a(Moxml::Element)
|
16
|
+
expect(doc.root.children.first.text).to eq("text")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "handles complex documents" do
|
20
|
+
xml = <<~XML
|
21
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
22
|
+
<root xmlns="http://example.org">
|
23
|
+
<!-- comment -->
|
24
|
+
<child id="1">
|
25
|
+
<![CDATA[cdata content]]>
|
26
|
+
</child>
|
27
|
+
<?pi target data?>
|
28
|
+
</root>
|
29
|
+
XML
|
30
|
+
|
31
|
+
doc = context.config.adapter.parse(xml)
|
32
|
+
|
33
|
+
expect(doc.root.namespaces.count).to eq(1)
|
34
|
+
expect(doc.root.namespaces.first.uri).to eq("http://example.org")
|
35
|
+
expect(doc.root.children[0]).to be_a(Moxml::Comment)
|
36
|
+
expect(doc.root.children[1]).to be_a(Moxml::Element)
|
37
|
+
expect(doc.root.children[1].name).to eq("child")
|
38
|
+
expect(doc.root.children[1]["id"]).to eq("1")
|
39
|
+
expect(doc.root.children[1].children.first).to be_a(Moxml::Cdata)
|
40
|
+
expect(doc.root.children[2]).to be_a(Moxml::ProcessingInstruction)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|