moxml 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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 +400 -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 +314 -0
  14. data/lib/moxml/adapter/oga.rb +309 -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 +61 -99
  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 +110 -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