moxml 0.1.0 → 0.1.2

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +15 -0
  3. data/.github/workflows/release.yml +23 -0
  4. data/.gitignore +3 -0
  5. data/.rubocop.yml +2 -0
  6. data/.rubocop_todo.yml +65 -0
  7. data/.ruby-version +1 -0
  8. data/Gemfile +10 -3
  9. data/README.adoc +401 -594
  10. data/lib/moxml/adapter/base.rb +102 -0
  11. data/lib/moxml/adapter/customized_oga/xml_declaration.rb +18 -0
  12. data/lib/moxml/adapter/customized_oga/xml_generator.rb +104 -0
  13. data/lib/moxml/adapter/nokogiri.rb +319 -0
  14. data/lib/moxml/adapter/oga.rb +318 -0
  15. data/lib/moxml/adapter/ox.rb +325 -0
  16. data/lib/moxml/adapter.rb +26 -170
  17. data/lib/moxml/attribute.rb +47 -14
  18. data/lib/moxml/builder.rb +64 -0
  19. data/lib/moxml/cdata.rb +4 -26
  20. data/lib/moxml/comment.rb +6 -22
  21. data/lib/moxml/config.rb +39 -15
  22. data/lib/moxml/context.rb +29 -0
  23. data/lib/moxml/declaration.rb +16 -26
  24. data/lib/moxml/doctype.rb +9 -0
  25. data/lib/moxml/document.rb +51 -63
  26. data/lib/moxml/document_builder.rb +87 -0
  27. data/lib/moxml/element.rb +63 -97
  28. data/lib/moxml/error.rb +20 -0
  29. data/lib/moxml/namespace.rb +12 -37
  30. data/lib/moxml/node.rb +78 -58
  31. data/lib/moxml/node_set.rb +19 -222
  32. data/lib/moxml/processing_instruction.rb +6 -25
  33. data/lib/moxml/text.rb +4 -26
  34. data/lib/moxml/version.rb +1 -1
  35. data/lib/moxml/xml_utils/encoder.rb +55 -0
  36. data/lib/moxml/xml_utils.rb +80 -0
  37. data/lib/moxml.rb +33 -33
  38. data/moxml.gemspec +1 -1
  39. data/spec/moxml/adapter/nokogiri_spec.rb +14 -0
  40. data/spec/moxml/adapter/oga_spec.rb +14 -0
  41. data/spec/moxml/adapter/ox_spec.rb +49 -0
  42. data/spec/moxml/all_with_adapters_spec.rb +46 -0
  43. data/spec/moxml/config_spec.rb +55 -0
  44. data/spec/moxml/error_spec.rb +71 -0
  45. data/spec/moxml/examples/adapter_spec.rb +27 -0
  46. data/spec/moxml_spec.rb +50 -0
  47. data/spec/spec_helper.rb +32 -0
  48. data/spec/support/shared_examples/attribute.rb +165 -0
  49. data/spec/support/shared_examples/builder.rb +25 -0
  50. data/spec/support/shared_examples/cdata.rb +70 -0
  51. data/spec/support/shared_examples/comment.rb +65 -0
  52. data/spec/support/shared_examples/context.rb +35 -0
  53. data/spec/support/shared_examples/declaration.rb +93 -0
  54. data/spec/support/shared_examples/doctype.rb +25 -0
  55. data/spec/support/shared_examples/document.rb +110 -0
  56. data/spec/support/shared_examples/document_builder.rb +43 -0
  57. data/spec/support/shared_examples/edge_cases.rb +185 -0
  58. data/spec/support/shared_examples/element.rb +130 -0
  59. data/spec/support/shared_examples/examples/attribute.rb +42 -0
  60. data/spec/support/shared_examples/examples/basic_usage.rb +67 -0
  61. data/spec/support/shared_examples/examples/memory.rb +54 -0
  62. data/spec/support/shared_examples/examples/namespace.rb +65 -0
  63. data/spec/support/shared_examples/examples/readme_examples.rb +100 -0
  64. data/spec/support/shared_examples/examples/thread_safety.rb +43 -0
  65. data/spec/support/shared_examples/examples/xpath.rb +39 -0
  66. data/spec/support/shared_examples/integration.rb +135 -0
  67. data/spec/support/shared_examples/namespace.rb +96 -0
  68. data/spec/support/shared_examples/node.rb +110 -0
  69. data/spec/support/shared_examples/node_set.rb +90 -0
  70. data/spec/support/shared_examples/processing_instruction.rb +88 -0
  71. data/spec/support/shared_examples/text.rb +66 -0
  72. data/spec/support/shared_examples/xml_adapter.rb +191 -0
  73. data/spec/support/xml_matchers.rb +27 -0
  74. metadata +55 -6
  75. data/.github/workflows/main.yml +0 -27
  76. data/lib/moxml/error_handler.rb +0 -77
  77. data/lib/moxml/errors.rb +0 -169
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples "XPath Examples" do
4
+ let(:context) { Moxml.new }
5
+
6
+ describe "XPath querying" do
7
+ let(:doc) do
8
+ context.parse(<<~XML)
9
+ <root xmlns:dc="http://purl.org/dc/elements/1.1/">
10
+ <book id="1">
11
+ <dc:title>First</dc:title>
12
+ </book>
13
+ <book id="2">
14
+ <dc:title>Second</dc:title>
15
+ </book>
16
+ </root>
17
+ XML
18
+ end
19
+
20
+ it "finds nodes by XPath" do
21
+ books = doc.xpath("//book")
22
+ expect(books.size).to eq(2)
23
+ expect(books.map { |b| b["id"] }).to eq(%w[1 2])
24
+ end
25
+
26
+ it "finds nodes with namespaces" do
27
+ titles = doc.xpath("//dc:title",
28
+ "dc" => "http://purl.org/dc/elements/1.1/")
29
+ expect(titles.map(&:text)).to eq(%w[First Second])
30
+ end
31
+
32
+ it "finds nodes by attributes" do
33
+ book = doc.at_xpath('//book[@id="2"]')
34
+ expect(book).not_to be_nil
35
+ expect(book.at_xpath(".//dc:title",
36
+ "dc" => "http://purl.org/dc/elements/1.1/").text).to eq("Second")
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples "Moxml Integration" do
4
+ let(:context) { Moxml.new }
5
+
6
+ describe "complete document workflow" do
7
+ let(:doc) { context.create_document }
8
+ before do
9
+ # Create document with declaration
10
+ doc.add_child(doc.create_declaration("1.0", "UTF-8"))
11
+
12
+ # Add root with namespaces
13
+ root = doc.create_element("root")
14
+ root.add_namespace(nil, "http://example.org")
15
+ root.add_namespace("xs", "http://www.w3.org/2001/XMLSchema")
16
+ doc.add_child(root)
17
+
18
+ # Add processing instruction
19
+ style_pi = doc.create_processing_instruction("xml-stylesheet", 'type="text/xsl" href="style.xsl"')
20
+ root.add_previous_sibling(style_pi)
21
+
22
+ # Add mixed content
23
+ root.add_child(doc.create_comment(" Mixed content example "))
24
+
25
+ text_node = doc.create_element("text")
26
+ text_node.add_child("Some text ")
27
+ text_node.add_child(doc.create_cdata("<with><markup/>"))
28
+ text_node.add_child(" and more text")
29
+ root.add_child(text_node)
30
+
31
+ # Add element with attributes
32
+ item = doc.create_element("item")
33
+ item["id"] = "123"
34
+ item["xs:type"] = "custom"
35
+ root.add_child(item)
36
+ end
37
+
38
+ it "has correct xml elements" do
39
+ # Verify structure
40
+ expected_xml_tags = [
41
+ '<?xml version="1.0" encoding="UTF-8"?>',
42
+ '<?xml-stylesheet type="text/xsl" href="style.xsl"?>',
43
+ '<root xmlns="http://example.org" xmlns:xs="http://www.w3.org/2001/XMLSchema">',
44
+ "<!-- Mixed content example -->",
45
+ "<text>Some text <![CDATA[<with><markup/>]]> and more text</text>",
46
+ '<item id="123" xs:type="custom"></item>',
47
+ "</root>"
48
+ ]
49
+
50
+ expected_xml_tags.each do |expected_xml|
51
+ # include(*expected_xml_tags) is shorter but worse for debugging
52
+ expect(doc.to_xml).to include(expected_xml)
53
+ end
54
+ end
55
+
56
+ it "handles xpath queries" do
57
+ # Test XPath queries
58
+ #
59
+ # XPath with a default namespace is a problem
60
+ # Nokogiri:
61
+ # https://timnew.me/blog/2012/10/25/pitfall-in-nokogiri-xpath-and-namespace/
62
+ # Oga:
63
+ # https://www.rubydoc.info/gems/oga/0.3.3#namespace-support
64
+ expect(doc.xpath('//xmlns:item[@id="123"]')).not_to be_empty
65
+ expect(doc.at_xpath("//xmlns:text").text).to include("Some text")
66
+ end
67
+ end
68
+
69
+ describe "namespace handling" do
70
+ it "handles complex namespace scenarios" do
71
+ xml = <<~XML
72
+ <root xmlns="http://default.org" xmlns:a="http://a.org" xmlns:b="http://b.org">
73
+ <child>
74
+ <a:element b:attr="value">
75
+ <deeper xmlns="http://other.org"/>
76
+ </a:element>
77
+ </child>
78
+ </root>
79
+ XML
80
+
81
+ doc = context.parse(xml)
82
+
83
+ # Test root namespace
84
+ root = doc.root
85
+ expect(root.namespace.prefix).to be_nil
86
+ expect(root.namespace.uri).to eq("http://default.org")
87
+
88
+ # Test namespace inheritance
89
+ child = doc.at_xpath("//xmlns:child")
90
+ expect(child.namespace.uri).to eq("http://default.org")
91
+
92
+ # Test namespace prefix resolution
93
+ a_element = doc.at_xpath("//a:element", "a" => "http://a.org")
94
+ expect(a_element).not_to be_nil
95
+ expect(a_element.namespace.prefix).to eq("a")
96
+
97
+ # Test attribute namespace
98
+ attr = a_element["b:attr"]
99
+ expect(attr).to eq("value")
100
+
101
+ # Test namespace override
102
+ deeper = a_element.children.first
103
+ expect(deeper.namespace.uri).to eq("http://other.org")
104
+ end
105
+ end
106
+
107
+ describe "document modification" do
108
+ let(:doc) { context.parse("<root><a>1</a><b>2</b></root>") }
109
+
110
+ it "handles complex modifications" do
111
+ # Move nodes
112
+ b_node = doc.at_xpath("//b")
113
+ a_node = doc.at_xpath("//a")
114
+ b_node.add_previous_sibling(a_node)
115
+
116
+ # Add nodes
117
+ c_node = doc.create_element("c")
118
+ c_node.add_child("3")
119
+ b_node.add_next_sibling(c_node)
120
+
121
+ # Modify attributes
122
+ doc.root["id"] = "main"
123
+
124
+ # Add mixed content
125
+ b_node.add_child(doc.create_comment(" comment "))
126
+ b_node.add_child(doc.create_cdata("<tag>"))
127
+
128
+ expect(doc.root.children.map(&:name)).to eq(%w[a b c])
129
+ expect(doc.to_xml).to include(
130
+ '<root id="main">',
131
+ "<b>2<!-- comment --><![CDATA[<tag>]]></b>"
132
+ )
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples "Moxml::Namespace" do
4
+ describe Moxml::Namespace do
5
+ let(:context) { Moxml.new }
6
+ let(:doc) { context.create_document }
7
+ let(:element) { doc.create_element("test") }
8
+
9
+ describe "creation" do
10
+ it "creates namespace with prefix" do
11
+ element.add_namespace("xs", "http://www.w3.org/2001/XMLSchema")
12
+ ns = element.namespaces.first
13
+
14
+ expect(ns).to be_namespace
15
+ expect(ns.prefix).to eq("xs")
16
+ expect(ns.uri).to eq("http://www.w3.org/2001/XMLSchema")
17
+ end
18
+
19
+ it "creates default namespace" do
20
+ element.add_namespace(nil, "http://example.org")
21
+ ns = element.namespaces.first
22
+
23
+ expect(ns.prefix).to be_nil
24
+ expect(ns.uri).to eq("http://example.org")
25
+ end
26
+
27
+ it "validates URI" do
28
+ expect do
29
+ element.add_namespace("xs", "invalid uri")
30
+ end.to raise_error(Moxml::NamespaceError, "Invalid URI: invalid uri")
31
+ end
32
+ end
33
+
34
+ describe "string representation" do
35
+ it "formats prefixed namespace" do
36
+ element.add_namespace("xs", "http://www.w3.org/2001/XMLSchema")
37
+ expect(element.namespaces.first.to_s).to eq('xmlns:xs="http://www.w3.org/2001/XMLSchema"')
38
+ end
39
+
40
+ it "formats default namespace" do
41
+ element.add_namespace(nil, "http://example.org")
42
+ expect(element.namespaces.first.to_s).to eq('xmlns="http://example.org"')
43
+ end
44
+ end
45
+
46
+ describe "equality" do
47
+ let(:ns1) { element.add_namespace("xs", "http://www.w3.org/2001/XMLSchema").namespaces.last }
48
+ let(:ns2) { element.add_namespace("xs", "http://www.w3.org/2001/XMLSchema").namespaces.last }
49
+ let(:ns3) { element.add_namespace("xsi", "http://www.w3.org/2001/XMLSchema-instance").namespaces.last }
50
+
51
+ it "compares namespaces" do
52
+ expect(ns1).to eq(ns2)
53
+ expect(ns1).not_to eq(ns3)
54
+ end
55
+
56
+ it "compares with different elements" do
57
+ other_element = doc.create_element("other")
58
+ other_ns = other_element.add_namespace("xs", "http://www.w3.org/2001/XMLSchema").namespaces.first
59
+ expect(ns1).to eq(other_ns)
60
+ end
61
+ end
62
+
63
+ describe "inheritance" do
64
+ it "does not inherit parent namespaces" do
65
+ # https://stackoverflow.com/a/67347081
66
+ root = doc.create_element("root")
67
+ root.namespace = { "xs" => "http://www.w3.org/2001/XMLSchema" }
68
+ child = doc.create_element("child")
69
+ root.add_child(child)
70
+
71
+ expect(child.namespace).to be_nil
72
+ end
73
+
74
+ it "inherits default parent namespaces" do
75
+ root = doc.create_element("root")
76
+ root.namespace = { nil => "http://www.w3.org/2001/XMLSchema" }
77
+ child = doc.create_element("child")
78
+ root.add_child(child)
79
+
80
+ expect(child.namespace.prefix).to be_nil
81
+ expect(child.namespace.uri).to eq("http://www.w3.org/2001/XMLSchema")
82
+ end
83
+
84
+ it "overrides parent namespace" do
85
+ root = doc.create_element("root")
86
+ root.namespace = { "ns" => "http://example.org/1" }
87
+ child = doc.create_element("child")
88
+ child.namespace = { "ns" => "http://example.org/2" }
89
+ root.add_child(child)
90
+
91
+ expect(root.namespace.uri).to eq("http://example.org/1")
92
+ expect(child.namespace.uri).to eq("http://example.org/2")
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples "Moxml::Node" do
4
+ describe Moxml::Node do
5
+ let(:context) { Moxml.new }
6
+ let(:doc) { context.parse("<root><child>text</child></root>") }
7
+ let(:root) { doc.root }
8
+ let(:child) { root.children.first }
9
+
10
+ describe "#document" do
11
+ it "returns containing document" do
12
+ expect(root.document).to eq(doc)
13
+ expect(child.document).to eq(doc)
14
+ end
15
+ end
16
+
17
+ describe "#parent" do
18
+ it "returns parent node" do
19
+ expect(child.parent).to eq(root)
20
+ expect(root.parent).to be_a(Moxml::Document)
21
+ end
22
+ end
23
+
24
+ describe "#children" do
25
+ it "returns node set of children" do
26
+ expect(root.children).to be_a(Moxml::NodeSet)
27
+ expect(root.children.size).to eq(1)
28
+ end
29
+ end
30
+
31
+ describe "sibling access" do
32
+ let(:doc) { context.parse("<root><a/>text<b/></root>") }
33
+ let(:first) { doc.root.children[0] }
34
+ let(:middle) { doc.root.children[1] }
35
+ let(:last) { doc.root.children[2] }
36
+
37
+ it "navigates between siblings" do
38
+ expect(first.next_sibling).to eq(middle)
39
+ expect(middle.next_sibling).to eq(last)
40
+ expect(last.previous_sibling).to eq(middle)
41
+ expect(middle.previous_sibling).to eq(first)
42
+ end
43
+ end
44
+
45
+ describe "node manipulation" do
46
+ it "adds child" do
47
+ new_child = doc.create_element("new")
48
+ root.add_child(new_child)
49
+ expect(root.children.last).to eq(new_child)
50
+ end
51
+
52
+ it "adds previous sibling" do
53
+ new_node = doc.create_element("new")
54
+ child.add_previous_sibling(new_node)
55
+ expect(root.children.first).to eq(new_node)
56
+ end
57
+
58
+ it "adds next sibling" do
59
+ new_node = doc.create_element("new")
60
+ child.add_next_sibling(new_node)
61
+ expect(root.children.last).to eq(new_node)
62
+ end
63
+
64
+ it "removes node" do
65
+ child.remove
66
+ expect(root.children).to be_empty
67
+ end
68
+
69
+ it "replaces node" do
70
+ new_node = doc.create_element("new")
71
+ child.replace(new_node)
72
+ expect(root.children.first).to eq(new_node)
73
+ end
74
+ end
75
+
76
+ describe "#to_xml" do
77
+ let(:context) { Moxml.new }
78
+ let(:doc) { context.parse("<root><child>text</child></root>") }
79
+
80
+ it "uses native serialization" do
81
+ expect(context.config.adapter).to receive(:serialize)
82
+ .with(doc.native, hash_including(indent: 2))
83
+
84
+ doc.to_xml(indent: 2)
85
+ end
86
+
87
+ it "passes through serialization options" do
88
+ expect(context.config.adapter).to receive(:serialize)
89
+ .with(doc.native, hash_including(encoding: "UTF-8", indent: 4))
90
+
91
+ doc.to_xml(encoding: "UTF-8", indent: 4)
92
+ end
93
+ end
94
+
95
+ describe "XPath support" do
96
+ let(:doc) { context.parse("<root><a><b>1</b></a><a><b>2</b></a></root>") }
97
+
98
+ it "finds nodes by xpath" do
99
+ nodes = doc.xpath("//b")
100
+ expect(nodes.size).to eq(2)
101
+ expect(nodes.map(&:text)).to eq(%w[1 2])
102
+ end
103
+
104
+ it "finds first node by xpath" do
105
+ node = doc.at_xpath("//b")
106
+ expect(node.text).to eq("1")
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples "Moxml::NodeSet" do
4
+ describe Moxml::NodeSet do
5
+ let(:context) { Moxml.new }
6
+ let(:xml) do
7
+ <<~XML
8
+ <root>
9
+ <child>First</child>
10
+ <child>Second</child>
11
+ <child>Third</child>
12
+ </root>
13
+ XML
14
+ end
15
+ let(:doc) { context.parse(xml) }
16
+ let(:nodes) { doc.xpath("//child") }
17
+
18
+ it "implements Enumerable" do
19
+ expect(nodes).to be_a(Enumerable)
20
+ end
21
+
22
+ it "is a NodeSet" do
23
+ expect(nodes).to be_a(described_class)
24
+ end
25
+
26
+ describe "enumeration" do
27
+ it "iterates over nodes" do
28
+ texts = []
29
+ nodes.each { |node| texts << node.text }
30
+ expect(texts).to eq(%w[First Second Third])
31
+ end
32
+
33
+ it "maps nodes" do
34
+ texts = nodes.map(&:text)
35
+ expect(texts).to eq(%w[First Second Third])
36
+ end
37
+
38
+ it "selects nodes" do
39
+ selected = nodes.select { |node| node.text.include?("i") }
40
+ expect(selected.size).to eq(2)
41
+ expect(selected.map(&:text)).to eq(%w[First Third])
42
+ end
43
+ end
44
+
45
+ describe "access methods" do
46
+ it "accesses by index" do
47
+ expect(nodes[0].text).to eq("First")
48
+ expect(nodes[1].text).to eq("Second")
49
+ expect(nodes[-1].text).to eq("Third")
50
+ end
51
+
52
+ it "accesses by range" do
53
+ subset = nodes[0..1]
54
+ expect(subset).to be_a(described_class)
55
+ expect(subset.size).to eq(2)
56
+ expect(subset.map(&:text)).to eq(%w[First Second])
57
+ end
58
+
59
+ it "provides first and last" do
60
+ expect(nodes.first.text).to eq("First")
61
+ expect(nodes.last.text).to eq("Third")
62
+ end
63
+ end
64
+
65
+ describe "modification methods" do
66
+ it "removes all nodes" do
67
+ nodes.remove
68
+ expect(doc.xpath("//child")).to be_empty
69
+ end
70
+
71
+ it "preserves document structure after removal" do
72
+ nodes.remove
73
+ expect(doc.root).not_to be_nil
74
+ expect(doc.root.name).to eq("root")
75
+ end
76
+ end
77
+
78
+ describe "concatenation" do
79
+ it "combines node sets" do
80
+ other_doc = context.parse("<root><item>Fourth</item></root>")
81
+ other_nodes = other_doc.xpath("//item")
82
+ combined = nodes + other_nodes
83
+
84
+ expect(combined).to be_a(described_class)
85
+ expect(combined.size).to eq(4)
86
+ expect(combined.map(&:text)).to eq(%w[First Second Third Fourth])
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples "Moxml::ProcessingInstruction" do
4
+ let(:context) { Moxml.new }
5
+ let(:doc) { context.create_document }
6
+ let(:pi) { doc.create_processing_instruction("xml-stylesheet", 'href="style.xsl" type="text/xsl"') }
7
+
8
+ it "identifies as processing instruction node" do
9
+ expect(pi).to be_processing_instruction
10
+ end
11
+
12
+ describe "target manipulation" do
13
+ it "gets target" do
14
+ expect(pi.target).to eq("xml-stylesheet")
15
+ end
16
+
17
+ it "sets target" do
18
+ pi.target = "new-target"
19
+ expect(pi.target).to eq("new-target")
20
+ end
21
+
22
+ it "handles nil target" do
23
+ pi.target = nil
24
+ expect(pi.target).to eq("")
25
+ end
26
+ end
27
+
28
+ describe "content manipulation" do
29
+ it "gets content" do
30
+ expect(pi.content).to eq('href="style.xsl" type="text/xsl"')
31
+ end
32
+
33
+ it "sets content" do
34
+ pi.content = 'href="new.xsl"'
35
+ expect(pi.content).to eq('href="new.xsl"')
36
+ end
37
+
38
+ it "handles nil content" do
39
+ pi.content = nil
40
+ expect(pi.content).to eq("")
41
+ end
42
+ end
43
+
44
+ describe "serialization" do
45
+ it "formats processing instruction" do
46
+ expect(pi.to_xml).to eq('<?xml-stylesheet href="style.xsl" type="text/xsl"?>')
47
+ end
48
+
49
+ it "handles special characters" do
50
+ pi.content = '< > & " \''
51
+ expect(pi.to_xml).to include('< > & " \'')
52
+ end
53
+ end
54
+
55
+ describe "node operations" do
56
+ let(:element) { doc.create_element("test") }
57
+
58
+ it "adds to element" do
59
+ element.add_child(pi)
60
+ expect(element.to_xml).to include("<?xml-stylesheet")
61
+ end
62
+
63
+ it "removes from element" do
64
+ element.add_child(pi)
65
+ pi.remove
66
+ expect(element.children).to be_empty
67
+ end
68
+
69
+ it "replaces with another node" do
70
+ element.add_child(pi)
71
+ text = doc.create_text("replacement")
72
+ pi.replace(text)
73
+ expect(element.text).to eq("replacement")
74
+ end
75
+ end
76
+
77
+ describe "common use cases" do
78
+ it "creates stylesheet instruction" do
79
+ pi = doc.create_processing_instruction("xml-stylesheet", 'type="text/xsl" href="style.xsl"')
80
+ expect(pi.to_xml).to eq('<?xml-stylesheet type="text/xsl" href="style.xsl"?>')
81
+ end
82
+
83
+ it "creates PHP instruction" do
84
+ pi = doc.create_processing_instruction("php", 'echo "Hello";')
85
+ expect(pi.to_xml).to eq('<?php echo "Hello";?>')
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples "Moxml::Text" do
4
+ let(:context) { Moxml.new }
5
+ let(:doc) { context.create_document }
6
+ let(:text) { doc.create_text("content") }
7
+
8
+ it "identifies as text node" do
9
+ expect(text).to be_text
10
+ end
11
+
12
+ describe "content manipulation" do
13
+ it "gets content" do
14
+ expect(text.content).to eq("content")
15
+ end
16
+
17
+ it "sets content" do
18
+ text.content = "new content"
19
+ expect(text.content).to eq("new content")
20
+ end
21
+
22
+ it "handles nil content" do
23
+ text.content = nil
24
+ expect(text.content).to eq("")
25
+ end
26
+
27
+ it "converts non-string content" do
28
+ text.content = 123
29
+ expect(text.content).to eq("123")
30
+ end
31
+ end
32
+
33
+ describe "special characters" do
34
+ it "encodes basic XML entities" do
35
+ text.content = "< > & \" '"
36
+ expect(text.to_xml).to eq("&lt; &gt; &amp; \" '")
37
+ end
38
+
39
+ it "preserves whitespace" do
40
+ text.content = " spaced content\n\t"
41
+ expect(text.content).to eq(" spaced content\n\t")
42
+ end
43
+ end
44
+
45
+ describe "node operations" do
46
+ let(:element) { doc.create_element("test") }
47
+
48
+ it "adds to element" do
49
+ element.add_child(text)
50
+ expect(element.text).to eq("content")
51
+ end
52
+
53
+ it "removes from element" do
54
+ element.add_child(text)
55
+ text.remove
56
+ expect(element.text).to eq("")
57
+ end
58
+
59
+ it "replaces with another node" do
60
+ element.add_child(text)
61
+ new_text = doc.create_text("replacement")
62
+ text.replace(new_text)
63
+ expect(element.text).to eq("replacement")
64
+ end
65
+ end
66
+ end