moxml 0.1.0 → 0.1.1
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/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
|