moxml 0.1.6 → 0.1.8

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 (215) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos.json +5 -0
  3. data/.github/workflows/dependent-tests.yml +20 -0
  4. data/.github/workflows/docs.yml +59 -0
  5. data/.github/workflows/rake.yml +12 -4
  6. data/.github/workflows/release.yml +5 -3
  7. data/.gitignore +37 -0
  8. data/.rubocop.yml +15 -7
  9. data/.rubocop_todo.yml +238 -40
  10. data/Gemfile +14 -9
  11. data/LICENSE.md +6 -2
  12. data/README.adoc +535 -373
  13. data/Rakefile +53 -0
  14. data/benchmarks/.gitignore +6 -0
  15. data/benchmarks/generate_report.rb +550 -0
  16. data/docs/Gemfile +13 -0
  17. data/docs/_config.yml +138 -0
  18. data/docs/_guides/advanced-features.adoc +87 -0
  19. data/docs/_guides/development-testing.adoc +165 -0
  20. data/docs/_guides/index.adoc +45 -0
  21. data/docs/_guides/modifying-xml.adoc +293 -0
  22. data/docs/_guides/parsing-xml.adoc +231 -0
  23. data/docs/_guides/sax-parsing.adoc +603 -0
  24. data/docs/_guides/working-with-documents.adoc +118 -0
  25. data/docs/_pages/adapter-compatibility.adoc +369 -0
  26. data/docs/_pages/adapters/headed-ox.adoc +237 -0
  27. data/docs/_pages/adapters/index.adoc +98 -0
  28. data/docs/_pages/adapters/libxml.adoc +286 -0
  29. data/docs/_pages/adapters/nokogiri.adoc +252 -0
  30. data/docs/_pages/adapters/oga.adoc +292 -0
  31. data/docs/_pages/adapters/ox.adoc +55 -0
  32. data/docs/_pages/adapters/rexml.adoc +293 -0
  33. data/docs/_pages/best-practices.adoc +430 -0
  34. data/docs/_pages/compatibility.adoc +468 -0
  35. data/docs/_pages/configuration.adoc +251 -0
  36. data/docs/_pages/error-handling.adoc +350 -0
  37. data/docs/_pages/headed-ox-limitations.adoc +558 -0
  38. data/docs/_pages/headed-ox.adoc +1025 -0
  39. data/docs/_pages/index.adoc +35 -0
  40. data/docs/_pages/installation.adoc +141 -0
  41. data/docs/_pages/node-api-reference.adoc +50 -0
  42. data/docs/_pages/performance.adoc +36 -0
  43. data/docs/_pages/quick-start.adoc +244 -0
  44. data/docs/_pages/thread-safety.adoc +29 -0
  45. data/docs/_references/document-api.adoc +408 -0
  46. data/docs/_references/index.adoc +48 -0
  47. data/docs/_tutorials/basic-usage.adoc +268 -0
  48. data/docs/_tutorials/builder-pattern.adoc +343 -0
  49. data/docs/_tutorials/index.adoc +33 -0
  50. data/docs/_tutorials/namespace-handling.adoc +325 -0
  51. data/docs/_tutorials/xpath-queries.adoc +359 -0
  52. data/docs/index.adoc +122 -0
  53. data/examples/README.md +124 -0
  54. data/examples/api_client/README.md +424 -0
  55. data/examples/api_client/api_client.rb +394 -0
  56. data/examples/api_client/example_response.xml +48 -0
  57. data/examples/headed_ox_example/README.md +90 -0
  58. data/examples/headed_ox_example/headed_ox_demo.rb +71 -0
  59. data/examples/rss_parser/README.md +194 -0
  60. data/examples/rss_parser/example_feed.xml +93 -0
  61. data/examples/rss_parser/rss_parser.rb +189 -0
  62. data/examples/sax_parsing/README.md +50 -0
  63. data/examples/sax_parsing/data_extractor.rb +75 -0
  64. data/examples/sax_parsing/example.xml +21 -0
  65. data/examples/sax_parsing/large_file.rb +78 -0
  66. data/examples/sax_parsing/simple_parser.rb +55 -0
  67. data/examples/web_scraper/README.md +352 -0
  68. data/examples/web_scraper/example_page.html +201 -0
  69. data/examples/web_scraper/web_scraper.rb +312 -0
  70. data/lib/moxml/adapter/base.rb +107 -28
  71. data/lib/moxml/adapter/customized_libxml/cdata.rb +28 -0
  72. data/lib/moxml/adapter/customized_libxml/comment.rb +24 -0
  73. data/lib/moxml/adapter/customized_libxml/declaration.rb +85 -0
  74. data/lib/moxml/adapter/customized_libxml/element.rb +39 -0
  75. data/lib/moxml/adapter/customized_libxml/node.rb +44 -0
  76. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +31 -0
  77. data/lib/moxml/adapter/customized_libxml/text.rb +27 -0
  78. data/lib/moxml/adapter/customized_oga/xml_generator.rb +1 -1
  79. data/lib/moxml/adapter/customized_ox/attribute.rb +28 -3
  80. data/lib/moxml/adapter/customized_ox/namespace.rb +0 -2
  81. data/lib/moxml/adapter/customized_ox/text.rb +0 -2
  82. data/lib/moxml/adapter/customized_rexml/formatter.rb +11 -6
  83. data/lib/moxml/adapter/headed_ox.rb +161 -0
  84. data/lib/moxml/adapter/libxml.rb +1548 -0
  85. data/lib/moxml/adapter/nokogiri.rb +121 -9
  86. data/lib/moxml/adapter/oga.rb +123 -12
  87. data/lib/moxml/adapter/ox.rb +283 -27
  88. data/lib/moxml/adapter/rexml.rb +127 -20
  89. data/lib/moxml/adapter.rb +21 -4
  90. data/lib/moxml/attribute.rb +6 -0
  91. data/lib/moxml/builder.rb +40 -4
  92. data/lib/moxml/config.rb +8 -3
  93. data/lib/moxml/context.rb +39 -1
  94. data/lib/moxml/doctype.rb +13 -1
  95. data/lib/moxml/document.rb +39 -6
  96. data/lib/moxml/document_builder.rb +27 -5
  97. data/lib/moxml/element.rb +71 -2
  98. data/lib/moxml/error.rb +175 -6
  99. data/lib/moxml/node.rb +94 -3
  100. data/lib/moxml/node_set.rb +34 -0
  101. data/lib/moxml/sax/block_handler.rb +194 -0
  102. data/lib/moxml/sax/element_handler.rb +124 -0
  103. data/lib/moxml/sax/handler.rb +113 -0
  104. data/lib/moxml/sax.rb +31 -0
  105. data/lib/moxml/version.rb +1 -1
  106. data/lib/moxml/xml_utils/encoder.rb +4 -4
  107. data/lib/moxml/xml_utils.rb +7 -4
  108. data/lib/moxml/xpath/ast/node.rb +159 -0
  109. data/lib/moxml/xpath/cache.rb +91 -0
  110. data/lib/moxml/xpath/compiler.rb +1768 -0
  111. data/lib/moxml/xpath/context.rb +26 -0
  112. data/lib/moxml/xpath/conversion.rb +124 -0
  113. data/lib/moxml/xpath/engine.rb +52 -0
  114. data/lib/moxml/xpath/errors.rb +101 -0
  115. data/lib/moxml/xpath/lexer.rb +304 -0
  116. data/lib/moxml/xpath/parser.rb +485 -0
  117. data/lib/moxml/xpath/ruby/generator.rb +269 -0
  118. data/lib/moxml/xpath/ruby/node.rb +193 -0
  119. data/lib/moxml/xpath.rb +37 -0
  120. data/lib/moxml.rb +5 -2
  121. data/moxml.gemspec +3 -1
  122. data/old-specs/moxml/adapter/customized_libxml/.gitkeep +6 -0
  123. data/spec/consistency/README.md +77 -0
  124. data/spec/{moxml/examples/adapter_spec.rb → consistency/adapter_parity_spec.rb} +4 -4
  125. data/spec/examples/README.md +75 -0
  126. data/spec/{support/shared_examples/examples/attribute.rb → examples/attribute_examples_spec.rb} +1 -1
  127. data/spec/{support/shared_examples/examples/basic_usage.rb → examples/basic_usage_spec.rb} +2 -2
  128. data/spec/{support/shared_examples/examples/namespace.rb → examples/namespace_examples_spec.rb} +3 -3
  129. data/spec/{support/shared_examples/examples/readme_examples.rb → examples/readme_examples_spec.rb} +6 -4
  130. data/spec/{support/shared_examples/examples/xpath.rb → examples/xpath_examples_spec.rb} +10 -6
  131. data/spec/integration/README.md +71 -0
  132. data/spec/{moxml/all_with_adapters_spec.rb → integration/all_adapters_spec.rb} +3 -2
  133. data/spec/integration/headed_ox_integration_spec.rb +326 -0
  134. data/spec/{support → integration}/shared_examples/edge_cases.rb +37 -10
  135. data/spec/integration/shared_examples/high_level/.gitkeep +0 -0
  136. data/spec/{support/shared_examples/context.rb → integration/shared_examples/high_level/context_behavior.rb} +2 -1
  137. data/spec/{support/shared_examples/integration.rb → integration/shared_examples/integration_workflows.rb} +23 -6
  138. data/spec/integration/shared_examples/node_wrappers/.gitkeep +0 -0
  139. data/spec/{support/shared_examples/cdata.rb → integration/shared_examples/node_wrappers/cdata_behavior.rb} +6 -1
  140. data/spec/{support/shared_examples/comment.rb → integration/shared_examples/node_wrappers/comment_behavior.rb} +2 -1
  141. data/spec/{support/shared_examples/declaration.rb → integration/shared_examples/node_wrappers/declaration_behavior.rb} +5 -2
  142. data/spec/{support/shared_examples/doctype.rb → integration/shared_examples/node_wrappers/doctype_behavior.rb} +2 -2
  143. data/spec/{support/shared_examples/document.rb → integration/shared_examples/node_wrappers/document_behavior.rb} +1 -1
  144. data/spec/{support/shared_examples/node.rb → integration/shared_examples/node_wrappers/node_behavior.rb} +9 -2
  145. data/spec/{support/shared_examples/node_set.rb → integration/shared_examples/node_wrappers/node_set_behavior.rb} +1 -18
  146. data/spec/{support/shared_examples/processing_instruction.rb → integration/shared_examples/node_wrappers/processing_instruction_behavior.rb} +6 -2
  147. data/spec/moxml/README.md +41 -0
  148. data/spec/moxml/adapter/.gitkeep +0 -0
  149. data/spec/moxml/adapter/README.md +61 -0
  150. data/spec/moxml/adapter/base_spec.rb +27 -0
  151. data/spec/moxml/adapter/headed_ox_spec.rb +311 -0
  152. data/spec/moxml/adapter/libxml_spec.rb +14 -0
  153. data/spec/moxml/adapter/ox_spec.rb +9 -8
  154. data/spec/moxml/adapter/shared_examples/.gitkeep +0 -0
  155. data/spec/{support/shared_examples/xml_adapter.rb → moxml/adapter/shared_examples/adapter_contract.rb} +39 -12
  156. data/spec/moxml/adapter_spec.rb +16 -0
  157. data/spec/moxml/attribute_spec.rb +30 -0
  158. data/spec/moxml/builder_spec.rb +33 -0
  159. data/spec/moxml/cdata_spec.rb +31 -0
  160. data/spec/moxml/comment_spec.rb +31 -0
  161. data/spec/moxml/config_spec.rb +3 -3
  162. data/spec/moxml/context_spec.rb +28 -0
  163. data/spec/moxml/declaration_spec.rb +36 -0
  164. data/spec/moxml/doctype_spec.rb +33 -0
  165. data/spec/moxml/document_builder_spec.rb +30 -0
  166. data/spec/moxml/document_spec.rb +105 -0
  167. data/spec/moxml/element_spec.rb +143 -0
  168. data/spec/moxml/error_spec.rb +266 -22
  169. data/spec/{moxml_spec.rb → moxml/moxml_spec.rb} +9 -9
  170. data/spec/moxml/namespace_spec.rb +32 -0
  171. data/spec/moxml/node_set_spec.rb +39 -0
  172. data/spec/moxml/node_spec.rb +37 -0
  173. data/spec/moxml/processing_instruction_spec.rb +34 -0
  174. data/spec/moxml/sax_spec.rb +1067 -0
  175. data/spec/moxml/text_spec.rb +31 -0
  176. data/spec/moxml/version_spec.rb +14 -0
  177. data/spec/moxml/xml_utils/.gitkeep +0 -0
  178. data/spec/moxml/xml_utils/encoder_spec.rb +27 -0
  179. data/spec/moxml/xml_utils_spec.rb +49 -0
  180. data/spec/moxml/xpath/ast/node_spec.rb +83 -0
  181. data/spec/moxml/xpath/axes_spec.rb +296 -0
  182. data/spec/moxml/xpath/cache_spec.rb +358 -0
  183. data/spec/moxml/xpath/compiler_spec.rb +406 -0
  184. data/spec/moxml/xpath/context_spec.rb +210 -0
  185. data/spec/moxml/xpath/conversion_spec.rb +365 -0
  186. data/spec/moxml/xpath/fixtures/sample.xml +25 -0
  187. data/spec/moxml/xpath/functions/boolean_functions_spec.rb +114 -0
  188. data/spec/moxml/xpath/functions/node_functions_spec.rb +145 -0
  189. data/spec/moxml/xpath/functions/numeric_functions_spec.rb +164 -0
  190. data/spec/moxml/xpath/functions/position_functions_spec.rb +93 -0
  191. data/spec/moxml/xpath/functions/special_functions_spec.rb +89 -0
  192. data/spec/moxml/xpath/functions/string_functions_spec.rb +381 -0
  193. data/spec/moxml/xpath/lexer_spec.rb +488 -0
  194. data/spec/moxml/xpath/parser_integration_spec.rb +210 -0
  195. data/spec/moxml/xpath/parser_spec.rb +364 -0
  196. data/spec/moxml/xpath/ruby/generator_spec.rb +421 -0
  197. data/spec/moxml/xpath/ruby/node_spec.rb +291 -0
  198. data/spec/moxml/xpath_capabilities_spec.rb +199 -0
  199. data/spec/moxml/xpath_spec.rb +77 -0
  200. data/spec/performance/README.md +83 -0
  201. data/spec/performance/benchmark_spec.rb +64 -0
  202. data/spec/{support/shared_examples/examples/memory.rb → performance/memory_usage_spec.rb} +3 -1
  203. data/spec/{support/shared_examples/examples/thread_safety.rb → performance/thread_safety_spec.rb} +3 -1
  204. data/spec/performance/xpath_benchmark_spec.rb +259 -0
  205. data/spec/spec_helper.rb +58 -1
  206. data/spec/support/xml_matchers.rb +1 -1
  207. metadata +176 -35
  208. data/lib/ox/node.rb +0 -9
  209. data/spec/support/shared_examples/examples/benchmark_spec.rb +0 -51
  210. /data/spec/{support/shared_examples/builder.rb → integration/shared_examples/high_level/builder_behavior.rb} +0 -0
  211. /data/spec/{support/shared_examples/document_builder.rb → integration/shared_examples/high_level/document_builder_behavior.rb} +0 -0
  212. /data/spec/{support/shared_examples/attribute.rb → integration/shared_examples/node_wrappers/attribute_behavior.rb} +0 -0
  213. /data/spec/{support/shared_examples/element.rb → integration/shared_examples/node_wrappers/element_behavior.rb} +0 -0
  214. /data/spec/{support/shared_examples/namespace.rb → integration/shared_examples/node_wrappers/namespace_behavior.rb} +0 -0
  215. /data/spec/{support/shared_examples/text.rb → integration/shared_examples/node_wrappers/text_behavior.rb} +0 -0
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Context do
6
+ let(:context) { described_class.new }
7
+
8
+ describe "#parse" do
9
+ it "parses XML string" do
10
+ doc = context.parse("<root><child/></root>")
11
+ expect(doc).to be_a(Moxml::Document)
12
+ expect(doc.root.name).to eq("root")
13
+ end
14
+ end
15
+
16
+ describe "#config" do
17
+ it "has a configuration" do
18
+ expect(context.config).to be_a(Moxml::Config)
19
+ end
20
+ end
21
+
22
+ describe "adapter access" do
23
+ it "provides adapter through config" do
24
+ expect(context.config.adapter).to be_a(Class)
25
+ expect(context.config.adapter.ancestors).to include(Moxml::Adapter::Base)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Declaration do
6
+ let(:context) { Moxml.new }
7
+ let(:doc) { context.create_document }
8
+
9
+ describe "#version" do
10
+ it "returns XML version" do
11
+ decl = doc.create_declaration("1.0", "UTF-8")
12
+ expect(decl.version).to eq("1.0")
13
+ end
14
+ end
15
+
16
+ describe "#encoding" do
17
+ it "returns encoding" do
18
+ decl = doc.create_declaration("1.0", "UTF-8")
19
+ expect(decl.encoding).to eq("UTF-8")
20
+ end
21
+ end
22
+
23
+ describe "creation" do
24
+ it "creates declaration with defaults" do
25
+ decl = doc.create_declaration
26
+ expect(decl).to be_a(described_class)
27
+ expect(decl.version).to eq("1.0")
28
+ end
29
+
30
+ it "creates declaration with custom values" do
31
+ decl = doc.create_declaration("1.0", "ISO-8859-1")
32
+ expect(decl.version).to eq("1.0")
33
+ expect(decl.encoding).to eq("ISO-8859-1")
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Doctype do
6
+ let(:context) { Moxml.new }
7
+ let(:doc) { context.create_document }
8
+
9
+ describe "#name" do
10
+ it "returns doctype name" do
11
+ skip "Doctype accessor methods not yet implemented in all adapters"
12
+ doctype = doc.create_doctype("root", nil, "test.dtd")
13
+ expect(doctype.name).to eq("root")
14
+ end
15
+ end
16
+
17
+ describe "#system_id" do
18
+ it "returns system identifier" do
19
+ skip "Doctype accessor methods not yet implemented in all adapters"
20
+ doctype = doc.create_doctype("root", nil, "test.dtd")
21
+ expect(doctype.system_id).to eq("test.dtd")
22
+ end
23
+ end
24
+
25
+ describe "creation" do
26
+ it "creates a doctype" do
27
+ skip "Doctype accessor methods not yet implemented in all adapters"
28
+ doctype = doc.create_doctype("html", nil, nil)
29
+ expect(doctype).to be_a(described_class)
30
+ expect(doctype.name).to eq("html")
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # DocumentBuilder is an internal class for parsing
6
+ # These tests verify it correctly builds Moxml document from native documents
7
+ RSpec.describe Moxml::DocumentBuilder do
8
+ let(:context) { Moxml.new }
9
+
10
+ describe "#build" do
11
+ it "builds a Moxml document from native document" do
12
+ native_doc = context.parse("<root><child>content</child></root>").native
13
+ builder = described_class.new(context)
14
+ doc = builder.build(native_doc)
15
+
16
+ expect(doc).to be_a(Moxml::Document)
17
+ expect(doc.root.name).to eq("root")
18
+ end
19
+ end
20
+
21
+ describe "element handling" do
22
+ it "handles nested elements" do
23
+ native_doc = context.parse("<parent><child1/><child2/></parent>").native
24
+ builder = described_class.new(context)
25
+ doc = builder.build(native_doc)
26
+
27
+ expect(doc.root.children.length).to eq(2)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Document do
6
+ let(:context) { Moxml.new }
7
+ let(:doc) { context.parse("<root/>") }
8
+
9
+ describe "document creation" do
10
+ it "creates a document via parsing" do
11
+ expect(doc).to be_a(described_class)
12
+ end
13
+
14
+ it "has a context" do
15
+ expect(doc.context).to be_a(Moxml::Context)
16
+ end
17
+
18
+ it "has a root element" do
19
+ expect(doc.root).to be_a(Moxml::Element)
20
+ expect(doc.root.name).to eq("root")
21
+ end
22
+ end
23
+
24
+ describe "#create_element" do
25
+ it "creates an element" do
26
+ element = doc.create_element("test")
27
+ expect(element).to be_a(Moxml::Element)
28
+ expect(element.name).to eq("test")
29
+ end
30
+ end
31
+
32
+ describe "#to_xml" do
33
+ it "serializes to XML" do
34
+ doc = context.parse("<root><child>text</child></root>")
35
+ xml = doc.to_xml
36
+ expect(xml).to include("<root>")
37
+ expect(xml).to include("<child>")
38
+ end
39
+ end
40
+
41
+ describe "convenience API methods" do
42
+ describe "#add_element" do
43
+ it "creates and adds element with attributes" do
44
+ fresh_doc = context.create_document
45
+ elem = fresh_doc.add_element("book", id: "123", title: "Ruby")
46
+
47
+ expect(elem).to be_a(Moxml::Element)
48
+ expect(elem.name).to eq("book")
49
+ expect(elem["id"]).to eq("123")
50
+ expect(elem["title"]).to eq("Ruby")
51
+ expect(fresh_doc.children).to include(elem)
52
+ end
53
+
54
+ it "accepts a block for further customization" do
55
+ fresh_doc = context.create_document
56
+ elem = fresh_doc.add_element("book", id: "123") do |e|
57
+ e.text = "Content"
58
+ end
59
+
60
+ expect(elem.text).to eq("Content")
61
+ end
62
+
63
+ it "returns the created element" do
64
+ fresh_doc = context.create_document
65
+ elem = fresh_doc.add_element("book")
66
+
67
+ expect(elem).to be_a(Moxml::Element)
68
+ expect(elem.name).to eq("book")
69
+ end
70
+ end
71
+
72
+ describe "#find" do
73
+ it "finds first element matching xpath" do
74
+ doc = context.parse("<root><book id='1'/><book id='2'/></root>")
75
+ result = doc.find("//book")
76
+
77
+ expect(result).to be_a(Moxml::Element)
78
+ expect(result["id"]).to eq("1")
79
+ end
80
+
81
+ it "returns nil when not found" do
82
+ result = doc.find("//nonexistent")
83
+
84
+ expect(result).to be_nil
85
+ end
86
+ end
87
+
88
+ describe "#find_all" do
89
+ it "finds all elements matching xpath" do
90
+ doc = context.parse("<root><book id='1'/><book id='2'/></root>")
91
+ results = doc.find_all("//book")
92
+
93
+ expect(results).to be_an(Array)
94
+ expect(results.length).to eq(2)
95
+ expect(results.map { |r| r["id"] }).to eq(%w[1 2])
96
+ end
97
+
98
+ it "returns empty array when not found" do
99
+ results = doc.find_all("//nonexistent")
100
+
101
+ expect(results).to eq([])
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Element do
6
+ let(:context) { Moxml.new }
7
+ let(:doc) { context.parse("<root><child>text</child></root>") }
8
+ let(:element) { doc.root }
9
+
10
+ describe "#name" do
11
+ it "returns the element name" do
12
+ expect(element.name).to eq("root")
13
+ end
14
+ end
15
+
16
+ describe "#children" do
17
+ it "returns child elements" do
18
+ children = element.children
19
+ expect(children).not_to be_empty
20
+ expect(children.first.name).to eq("child")
21
+ end
22
+ end
23
+
24
+ describe "#[]" do
25
+ it "gets attribute value" do
26
+ elem = context.parse('<root id="123"/>').root
27
+ expect(elem["id"]).to eq("123")
28
+ end
29
+ end
30
+
31
+ describe "#[]=" do
32
+ it "sets attribute value" do
33
+ elem = doc.create_element("test")
34
+ elem["id"] = "456"
35
+ expect(elem["id"]).to eq("456")
36
+ end
37
+ end
38
+
39
+ describe "#text" do
40
+ it "returns text content" do
41
+ child = element.children.first
42
+ expect(child.text).to eq("text")
43
+ end
44
+ end
45
+
46
+ describe "convenience API methods" do
47
+ describe "#set_attributes" do
48
+ it "sets multiple attributes at once" do
49
+ elem = doc.create_element("book")
50
+ elem.set_attributes(id: "123", title: "Ruby", year: "2024")
51
+
52
+ expect(elem["id"]).to eq("123")
53
+ expect(elem["title"]).to eq("Ruby")
54
+ expect(elem["year"]).to eq("2024")
55
+ end
56
+
57
+ it "returns self for chaining" do
58
+ elem = doc.create_element("book")
59
+ result = elem.set_attributes(id: "123")
60
+
61
+ expect(result).to eq(elem)
62
+ end
63
+
64
+ it "handles empty hash" do
65
+ elem = doc.create_element("book")
66
+ expect { elem.set_attributes({}) }.not_to raise_error
67
+ end
68
+ end
69
+
70
+ describe "#with_child" do
71
+ it "adds a child and returns self" do
72
+ elem = doc.create_element("parent")
73
+ child = doc.create_element("child")
74
+
75
+ result = elem.with_child(child)
76
+
77
+ expect(result).to eq(elem)
78
+ expect(elem.children.map(&:name)).to include("child")
79
+ end
80
+
81
+ it "can be chained" do
82
+ elem = doc.create_element("parent")
83
+ child1 = doc.create_element("child1")
84
+ child2 = doc.create_element("child2")
85
+
86
+ elem.with_child(child1).with_child(child2)
87
+
88
+ expect(elem.children.map(&:name)).to eq(%w[child1 child2])
89
+ end
90
+ end
91
+
92
+ describe "#find_element" do
93
+ it "finds first element matching xpath" do
94
+ doc = context.parse("<root><book id='1'/><book id='2'/></root>")
95
+ result = doc.root.find_element(".//book")
96
+
97
+ expect(result).to be_a(described_class)
98
+ expect(result["id"]).to eq("1")
99
+ end
100
+
101
+ it "returns nil when not found" do
102
+ result = element.find_element(".//nonexistent")
103
+
104
+ expect(result).to be_nil
105
+ end
106
+ end
107
+
108
+ describe "#find_all" do
109
+ it "finds all elements matching xpath" do
110
+ doc = context.parse("<root><book id='1'/><book id='2'/></root>")
111
+ results = doc.root.find_all(".//book")
112
+
113
+ expect(results).to be_an(Array)
114
+ expect(results.length).to eq(2)
115
+ expect(results.map { |r| r["id"] }).to eq(%w[1 2])
116
+ end
117
+
118
+ it "returns empty array when not found" do
119
+ results = element.find_all(".//nonexistent")
120
+
121
+ expect(results).to eq([])
122
+ end
123
+ end
124
+
125
+ describe "method chaining" do
126
+ it "chains with_namespace, set_attributes, and with_child" do
127
+ elem = doc.create_element("book")
128
+ child = doc.create_element("title")
129
+ child.text = "Ruby Programming"
130
+
131
+ elem
132
+ .with_namespace("dc", "http://purl.org/dc/elements/1.1/")
133
+ .set_attributes(id: "123", type: "technical")
134
+ .with_child(child)
135
+
136
+ expect(elem["id"]).to eq("123")
137
+ expect(elem["type"]).to eq("technical")
138
+ expect(elem.children.first.name).to eq("title")
139
+ expect(elem.namespaces.map(&:uri)).to include("http://purl.org/dc/elements/1.1/")
140
+ end
141
+ end
142
+ end
143
+ end