moxml 0.1.7 → 0.1.9

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 +10 -10
  6. data/.github/workflows/release.yml +5 -3
  7. data/.gitignore +37 -0
  8. data/.rubocop.yml +15 -7
  9. data/.rubocop_todo.yml +224 -43
  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 +51 -0
  21. data/docs/_guides/modifying-xml.adoc +292 -0
  22. data/docs/_guides/parsing-xml.adoc +230 -0
  23. data/docs/_guides/sax-parsing.adoc +603 -0
  24. data/docs/_guides/working-with-documents.adoc +118 -0
  25. data/docs/_guides/xml-declaration.adoc +450 -0
  26. data/docs/_pages/adapter-compatibility.adoc +369 -0
  27. data/docs/_pages/adapters/headed-ox.adoc +237 -0
  28. data/docs/_pages/adapters/index.adoc +97 -0
  29. data/docs/_pages/adapters/libxml.adoc +285 -0
  30. data/docs/_pages/adapters/nokogiri.adoc +251 -0
  31. data/docs/_pages/adapters/oga.adoc +291 -0
  32. data/docs/_pages/adapters/ox.adoc +56 -0
  33. data/docs/_pages/adapters/rexml.adoc +292 -0
  34. data/docs/_pages/best-practices.adoc +429 -0
  35. data/docs/_pages/compatibility.adoc +467 -0
  36. data/docs/_pages/configuration.adoc +250 -0
  37. data/docs/_pages/error-handling.adoc +349 -0
  38. data/docs/_pages/headed-ox-limitations.adoc +574 -0
  39. data/docs/_pages/headed-ox.adoc +1025 -0
  40. data/docs/_pages/index.adoc +35 -0
  41. data/docs/_pages/installation.adoc +140 -0
  42. data/docs/_pages/node-api-reference.adoc +49 -0
  43. data/docs/_pages/performance.adoc +35 -0
  44. data/docs/_pages/quick-start.adoc +243 -0
  45. data/docs/_pages/thread-safety.adoc +28 -0
  46. data/docs/_references/document-api.adoc +407 -0
  47. data/docs/_references/index.adoc +48 -0
  48. data/docs/_tutorials/basic-usage.adoc +267 -0
  49. data/docs/_tutorials/builder-pattern.adoc +342 -0
  50. data/docs/_tutorials/index.adoc +33 -0
  51. data/docs/_tutorials/namespace-handling.adoc +324 -0
  52. data/docs/_tutorials/xpath-queries.adoc +358 -0
  53. data/docs/index.adoc +122 -0
  54. data/examples/README.md +124 -0
  55. data/examples/api_client/README.md +424 -0
  56. data/examples/api_client/api_client.rb +394 -0
  57. data/examples/api_client/example_response.xml +48 -0
  58. data/examples/headed_ox_example/README.md +90 -0
  59. data/examples/headed_ox_example/headed_ox_demo.rb +71 -0
  60. data/examples/rss_parser/README.md +194 -0
  61. data/examples/rss_parser/example_feed.xml +93 -0
  62. data/examples/rss_parser/rss_parser.rb +189 -0
  63. data/examples/sax_parsing/README.md +50 -0
  64. data/examples/sax_parsing/data_extractor.rb +75 -0
  65. data/examples/sax_parsing/example.xml +21 -0
  66. data/examples/sax_parsing/large_file.rb +78 -0
  67. data/examples/sax_parsing/simple_parser.rb +55 -0
  68. data/examples/web_scraper/README.md +352 -0
  69. data/examples/web_scraper/example_page.html +201 -0
  70. data/examples/web_scraper/web_scraper.rb +312 -0
  71. data/lib/moxml/adapter/base.rb +107 -28
  72. data/lib/moxml/adapter/customized_libxml/cdata.rb +28 -0
  73. data/lib/moxml/adapter/customized_libxml/comment.rb +24 -0
  74. data/lib/moxml/adapter/customized_libxml/declaration.rb +85 -0
  75. data/lib/moxml/adapter/customized_libxml/element.rb +39 -0
  76. data/lib/moxml/adapter/customized_libxml/node.rb +44 -0
  77. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +31 -0
  78. data/lib/moxml/adapter/customized_libxml/text.rb +27 -0
  79. data/lib/moxml/adapter/customized_oga/xml_generator.rb +1 -1
  80. data/lib/moxml/adapter/customized_ox/attribute.rb +28 -1
  81. data/lib/moxml/adapter/customized_rexml/formatter.rb +13 -8
  82. data/lib/moxml/adapter/headed_ox.rb +161 -0
  83. data/lib/moxml/adapter/libxml.rb +1564 -0
  84. data/lib/moxml/adapter/nokogiri.rb +156 -9
  85. data/lib/moxml/adapter/oga.rb +190 -15
  86. data/lib/moxml/adapter/ox.rb +322 -28
  87. data/lib/moxml/adapter/rexml.rb +157 -28
  88. data/lib/moxml/adapter.rb +21 -4
  89. data/lib/moxml/attribute.rb +6 -0
  90. data/lib/moxml/builder.rb +40 -4
  91. data/lib/moxml/config.rb +8 -3
  92. data/lib/moxml/context.rb +57 -2
  93. data/lib/moxml/declaration.rb +9 -0
  94. data/lib/moxml/doctype.rb +13 -1
  95. data/lib/moxml/document.rb +53 -6
  96. data/lib/moxml/document_builder.rb +34 -5
  97. data/lib/moxml/element.rb +71 -2
  98. data/lib/moxml/error.rb +175 -6
  99. data/lib/moxml/node.rb +155 -4
  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 +1770 -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 -5
  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_preservation_spec.rb +217 -0
  164. data/spec/moxml/declaration_spec.rb +36 -0
  165. data/spec/moxml/doctype_spec.rb +33 -0
  166. data/spec/moxml/document_builder_spec.rb +30 -0
  167. data/spec/moxml/document_spec.rb +105 -0
  168. data/spec/moxml/element_spec.rb +143 -0
  169. data/spec/moxml/error_spec.rb +266 -22
  170. data/spec/{moxml_spec.rb → moxml/moxml_spec.rb} +9 -9
  171. data/spec/moxml/namespace_spec.rb +32 -0
  172. data/spec/moxml/node_set_spec.rb +39 -0
  173. data/spec/moxml/node_spec.rb +37 -0
  174. data/spec/moxml/processing_instruction_spec.rb +34 -0
  175. data/spec/moxml/sax_spec.rb +1067 -0
  176. data/spec/moxml/text_spec.rb +31 -0
  177. data/spec/moxml/version_spec.rb +14 -0
  178. data/spec/moxml/xml_utils/.gitkeep +0 -0
  179. data/spec/moxml/xml_utils/encoder_spec.rb +27 -0
  180. data/spec/moxml/xml_utils_spec.rb +49 -0
  181. data/spec/moxml/xpath/ast/node_spec.rb +83 -0
  182. data/spec/moxml/xpath/axes_spec.rb +296 -0
  183. data/spec/moxml/xpath/cache_spec.rb +358 -0
  184. data/spec/moxml/xpath/compiler_spec.rb +406 -0
  185. data/spec/moxml/xpath/context_spec.rb +210 -0
  186. data/spec/moxml/xpath/conversion_spec.rb +365 -0
  187. data/spec/moxml/xpath/fixtures/sample.xml +25 -0
  188. data/spec/moxml/xpath/functions/boolean_functions_spec.rb +114 -0
  189. data/spec/moxml/xpath/functions/node_functions_spec.rb +145 -0
  190. data/spec/moxml/xpath/functions/numeric_functions_spec.rb +164 -0
  191. data/spec/moxml/xpath/functions/position_functions_spec.rb +93 -0
  192. data/spec/moxml/xpath/functions/special_functions_spec.rb +89 -0
  193. data/spec/moxml/xpath/functions/string_functions_spec.rb +381 -0
  194. data/spec/moxml/xpath/lexer_spec.rb +488 -0
  195. data/spec/moxml/xpath/parser_integration_spec.rb +210 -0
  196. data/spec/moxml/xpath/parser_spec.rb +364 -0
  197. data/spec/moxml/xpath/ruby/generator_spec.rb +421 -0
  198. data/spec/moxml/xpath/ruby/node_spec.rb +291 -0
  199. data/spec/moxml/xpath_capabilities_spec.rb +199 -0
  200. data/spec/moxml/xpath_spec.rb +77 -0
  201. data/spec/performance/README.md +83 -0
  202. data/spec/performance/benchmark_spec.rb +64 -0
  203. data/spec/{support/shared_examples/examples/memory.rb → performance/memory_usage_spec.rb} +4 -1
  204. data/spec/{support/shared_examples/examples/thread_safety.rb → performance/thread_safety_spec.rb} +3 -1
  205. data/spec/performance/xpath_benchmark_spec.rb +259 -0
  206. data/spec/spec_helper.rb +58 -1
  207. data/spec/support/xml_matchers.rb +1 -1
  208. metadata +178 -34
  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,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "XML Declaration Preservation" do
6
+ # Test with all available adapters
7
+ ADAPTERS = %i[nokogiri oga rexml ox libxml headed_ox].freeze
8
+
9
+ ADAPTERS.each do |adapter_name|
10
+ context "with #{adapter_name} adapter" do
11
+ let(:context) { Moxml.new(adapter_name) }
12
+
13
+ describe "automatic preservation" do
14
+ context "when input has no XML declaration" do
15
+ let(:xml_without_decl) { '<svg xmlns="http://www.w3.org/2000/svg"><rect/></svg>' }
16
+
17
+ it "does not add XML declaration to output" do
18
+ doc = context.parse(xml_without_decl)
19
+ output = doc.to_xml
20
+
21
+ expect(output).not_to include("<?xml")
22
+ expect(output).to include("<svg")
23
+ end
24
+
25
+ it "sets has_xml_declaration to false" do
26
+ doc = context.parse(xml_without_decl)
27
+ expect(doc.has_xml_declaration).to be false
28
+ end
29
+ end
30
+
31
+ context "when input has XML declaration" do
32
+ let(:xml_with_decl) do
33
+ '<?xml version="1.0" encoding="UTF-8"?><root><child/></root>'
34
+ end
35
+
36
+ it "preserves XML declaration in output" do
37
+ doc = context.parse(xml_with_decl)
38
+ output = doc.to_xml
39
+
40
+ expect(output).to include("<?xml")
41
+ expect(output).to include('version="1.0"')
42
+ end
43
+
44
+ it "sets has_xml_declaration to true" do
45
+ doc = context.parse(xml_with_decl)
46
+ expect(doc.has_xml_declaration).to be true
47
+ end
48
+ end
49
+
50
+ context "when input has declaration with standalone attribute" do
51
+ let(:xml_with_standalone) do
52
+ '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><root/>'
53
+ end
54
+
55
+ it "preserves the declaration" do
56
+ doc = context.parse(xml_with_standalone)
57
+ output = doc.to_xml
58
+
59
+ expect(output).to include("<?xml")
60
+ end
61
+ end
62
+ end
63
+
64
+ describe "explicit override" do
65
+ let(:xml_without_decl) { "<root><child/></root>" }
66
+ let(:xml_with_decl) { '<?xml version="1.0"?><root><child/></root>' }
67
+
68
+ context "when forcing declaration on document without one" do
69
+ it "adds declaration when declaration: true" do
70
+ doc = context.parse(xml_without_decl)
71
+ output = doc.to_xml(declaration: true)
72
+
73
+ expect(output).to include("<?xml")
74
+ end
75
+ end
76
+
77
+ context "when removing declaration from document with one" do
78
+ it "removes declaration when declaration: false" do
79
+ doc = context.parse(xml_with_decl)
80
+ output = doc.to_xml(declaration: false)
81
+
82
+ expect(output).not_to include("<?xml")
83
+ expect(output).to include("<root")
84
+ end
85
+ end
86
+
87
+ context "when explicitly preserving declaration" do
88
+ it "keeps declaration when declaration: true" do
89
+ doc = context.parse(xml_with_decl)
90
+ output = doc.to_xml(declaration: true)
91
+
92
+ expect(output).to include("<?xml")
93
+ end
94
+ end
95
+ end
96
+
97
+ describe "round-trip fidelity" do
98
+ context "for document without declaration" do
99
+ let(:original) { "<root><item id=\"1\"/></root>" }
100
+
101
+ it "maintains absence of declaration through parse and serialize" do
102
+ doc = context.parse(original)
103
+ output = doc.to_xml
104
+
105
+ expect(output).not_to include("<?xml")
106
+
107
+ # Parse again and verify
108
+ doc2 = context.parse(output)
109
+ expect(doc2.has_xml_declaration).to be false
110
+ end
111
+ end
112
+
113
+ context "for document with declaration" do
114
+ let(:original) do
115
+ '<?xml version="1.0" encoding="UTF-8"?><root><item id="1"/></root>'
116
+ end
117
+
118
+ it "maintains presence of declaration through parse and serialize" do
119
+ doc = context.parse(original)
120
+ output = doc.to_xml
121
+
122
+ expect(output).to include("<?xml")
123
+
124
+ # Parse again and verify
125
+ doc2 = context.parse(output)
126
+ expect(doc2.has_xml_declaration).to be true
127
+ end
128
+ end
129
+ end
130
+
131
+ describe "edge cases" do
132
+ context "with empty document" do
133
+ it "does not add declaration to empty document" do
134
+ doc = context.create_document
135
+
136
+ # Empty documents should not have declaration by default
137
+ expect(doc.has_xml_declaration).to be false
138
+ end
139
+ end
140
+
141
+ context "with built document" do
142
+ it "does not add declaration to programmatically built document" do
143
+ doc = context.create_document
144
+ root = doc.create_element("root")
145
+ doc.root = root
146
+
147
+ output = doc.to_xml
148
+
149
+ expect(output).not_to include("<?xml")
150
+ expect(doc.has_xml_declaration).to be false
151
+ end
152
+
153
+ it "can explicitly add declaration to built document" do
154
+ doc = context.create_document
155
+ root = doc.create_element("root")
156
+ doc.root = root
157
+
158
+ output = doc.to_xml(declaration: true)
159
+
160
+ expect(output).to include("<?xml")
161
+ end
162
+ end
163
+ end
164
+
165
+ describe "non-document nodes" do
166
+ let(:xml) { '<?xml version="1.0"?><root><child>text</child></root>' }
167
+
168
+ it "does not add declaration when serializing element nodes" do
169
+ doc = context.parse(xml)
170
+ root = doc.root
171
+ output = root.to_xml
172
+
173
+ expect(output).not_to include("<?xml")
174
+ expect(output).to include("<root>")
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ describe "integration with svg_conform use case" do
181
+ let(:context) { Moxml.new }
182
+
183
+ context "remediating SVG without declaration" do
184
+ let(:svg_input) { '<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect x="10" y="10" width="80" height="80"/></svg>' }
185
+
186
+ it "does not add declaration to remediated output" do
187
+ doc = context.parse(svg_input)
188
+
189
+ # Simulate remediation: add viewport
190
+ root = doc.root
191
+ root["viewBox"] = "0 0 100 100"
192
+
193
+ output = doc.to_xml
194
+
195
+ expect(output).not_to include("<?xml")
196
+ expect(output).to include('viewBox="0 0 100 100"')
197
+ end
198
+ end
199
+
200
+ context "remediating SVG with declaration" do
201
+ let(:svg_input) { '<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg"><rect/></svg>' }
202
+
203
+ it "preserves declaration in remediated output" do
204
+ doc = context.parse(svg_input)
205
+
206
+ # Simulate remediation
207
+ root = doc.root
208
+ root["viewBox"] = "0 0 100 100"
209
+
210
+ output = doc.to_xml
211
+
212
+ expect(output).to include("<?xml")
213
+ expect(output).to include('viewBox="0 0 100 100"')
214
+ end
215
+ end
216
+ end
217
+ 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