moxml 0.1.7 → 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 (212) 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 +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 -1
  80. data/lib/moxml/adapter/customized_rexml/formatter.rb +11 -6
  81. data/lib/moxml/adapter/headed_ox.rb +161 -0
  82. data/lib/moxml/adapter/libxml.rb +1548 -0
  83. data/lib/moxml/adapter/nokogiri.rb +121 -9
  84. data/lib/moxml/adapter/oga.rb +123 -12
  85. data/lib/moxml/adapter/ox.rb +282 -26
  86. data/lib/moxml/adapter/rexml.rb +127 -20
  87. data/lib/moxml/adapter.rb +21 -4
  88. data/lib/moxml/attribute.rb +6 -0
  89. data/lib/moxml/builder.rb +40 -4
  90. data/lib/moxml/config.rb +8 -3
  91. data/lib/moxml/context.rb +39 -1
  92. data/lib/moxml/doctype.rb +13 -1
  93. data/lib/moxml/document.rb +39 -6
  94. data/lib/moxml/document_builder.rb +27 -5
  95. data/lib/moxml/element.rb +71 -2
  96. data/lib/moxml/error.rb +175 -6
  97. data/lib/moxml/node.rb +94 -3
  98. data/lib/moxml/node_set.rb +34 -0
  99. data/lib/moxml/sax/block_handler.rb +194 -0
  100. data/lib/moxml/sax/element_handler.rb +124 -0
  101. data/lib/moxml/sax/handler.rb +113 -0
  102. data/lib/moxml/sax.rb +31 -0
  103. data/lib/moxml/version.rb +1 -1
  104. data/lib/moxml/xml_utils/encoder.rb +4 -4
  105. data/lib/moxml/xml_utils.rb +7 -4
  106. data/lib/moxml/xpath/ast/node.rb +159 -0
  107. data/lib/moxml/xpath/cache.rb +91 -0
  108. data/lib/moxml/xpath/compiler.rb +1768 -0
  109. data/lib/moxml/xpath/context.rb +26 -0
  110. data/lib/moxml/xpath/conversion.rb +124 -0
  111. data/lib/moxml/xpath/engine.rb +52 -0
  112. data/lib/moxml/xpath/errors.rb +101 -0
  113. data/lib/moxml/xpath/lexer.rb +304 -0
  114. data/lib/moxml/xpath/parser.rb +485 -0
  115. data/lib/moxml/xpath/ruby/generator.rb +269 -0
  116. data/lib/moxml/xpath/ruby/node.rb +193 -0
  117. data/lib/moxml/xpath.rb +37 -0
  118. data/lib/moxml.rb +5 -2
  119. data/moxml.gemspec +3 -1
  120. data/old-specs/moxml/adapter/customized_libxml/.gitkeep +6 -0
  121. data/spec/consistency/README.md +77 -0
  122. data/spec/{moxml/examples/adapter_spec.rb → consistency/adapter_parity_spec.rb} +4 -4
  123. data/spec/examples/README.md +75 -0
  124. data/spec/{support/shared_examples/examples/attribute.rb → examples/attribute_examples_spec.rb} +1 -1
  125. data/spec/{support/shared_examples/examples/basic_usage.rb → examples/basic_usage_spec.rb} +2 -2
  126. data/spec/{support/shared_examples/examples/namespace.rb → examples/namespace_examples_spec.rb} +3 -3
  127. data/spec/{support/shared_examples/examples/readme_examples.rb → examples/readme_examples_spec.rb} +6 -4
  128. data/spec/{support/shared_examples/examples/xpath.rb → examples/xpath_examples_spec.rb} +10 -6
  129. data/spec/integration/README.md +71 -0
  130. data/spec/{moxml/all_with_adapters_spec.rb → integration/all_adapters_spec.rb} +3 -2
  131. data/spec/integration/headed_ox_integration_spec.rb +326 -0
  132. data/spec/{support → integration}/shared_examples/edge_cases.rb +37 -10
  133. data/spec/integration/shared_examples/high_level/.gitkeep +0 -0
  134. data/spec/{support/shared_examples/context.rb → integration/shared_examples/high_level/context_behavior.rb} +2 -1
  135. data/spec/{support/shared_examples/integration.rb → integration/shared_examples/integration_workflows.rb} +23 -6
  136. data/spec/integration/shared_examples/node_wrappers/.gitkeep +0 -0
  137. data/spec/{support/shared_examples/cdata.rb → integration/shared_examples/node_wrappers/cdata_behavior.rb} +6 -1
  138. data/spec/{support/shared_examples/comment.rb → integration/shared_examples/node_wrappers/comment_behavior.rb} +2 -1
  139. data/spec/{support/shared_examples/declaration.rb → integration/shared_examples/node_wrappers/declaration_behavior.rb} +5 -2
  140. data/spec/{support/shared_examples/doctype.rb → integration/shared_examples/node_wrappers/doctype_behavior.rb} +2 -2
  141. data/spec/{support/shared_examples/document.rb → integration/shared_examples/node_wrappers/document_behavior.rb} +1 -1
  142. data/spec/{support/shared_examples/node.rb → integration/shared_examples/node_wrappers/node_behavior.rb} +9 -2
  143. data/spec/{support/shared_examples/node_set.rb → integration/shared_examples/node_wrappers/node_set_behavior.rb} +1 -18
  144. data/spec/{support/shared_examples/processing_instruction.rb → integration/shared_examples/node_wrappers/processing_instruction_behavior.rb} +6 -2
  145. data/spec/moxml/README.md +41 -0
  146. data/spec/moxml/adapter/.gitkeep +0 -0
  147. data/spec/moxml/adapter/README.md +61 -0
  148. data/spec/moxml/adapter/base_spec.rb +27 -0
  149. data/spec/moxml/adapter/headed_ox_spec.rb +311 -0
  150. data/spec/moxml/adapter/libxml_spec.rb +14 -0
  151. data/spec/moxml/adapter/ox_spec.rb +9 -8
  152. data/spec/moxml/adapter/shared_examples/.gitkeep +0 -0
  153. data/spec/{support/shared_examples/xml_adapter.rb → moxml/adapter/shared_examples/adapter_contract.rb} +39 -12
  154. data/spec/moxml/adapter_spec.rb +16 -0
  155. data/spec/moxml/attribute_spec.rb +30 -0
  156. data/spec/moxml/builder_spec.rb +33 -0
  157. data/spec/moxml/cdata_spec.rb +31 -0
  158. data/spec/moxml/comment_spec.rb +31 -0
  159. data/spec/moxml/config_spec.rb +3 -3
  160. data/spec/moxml/context_spec.rb +28 -0
  161. data/spec/moxml/declaration_spec.rb +36 -0
  162. data/spec/moxml/doctype_spec.rb +33 -0
  163. data/spec/moxml/document_builder_spec.rb +30 -0
  164. data/spec/moxml/document_spec.rb +105 -0
  165. data/spec/moxml/element_spec.rb +143 -0
  166. data/spec/moxml/error_spec.rb +266 -22
  167. data/spec/{moxml_spec.rb → moxml/moxml_spec.rb} +9 -9
  168. data/spec/moxml/namespace_spec.rb +32 -0
  169. data/spec/moxml/node_set_spec.rb +39 -0
  170. data/spec/moxml/node_spec.rb +37 -0
  171. data/spec/moxml/processing_instruction_spec.rb +34 -0
  172. data/spec/moxml/sax_spec.rb +1067 -0
  173. data/spec/moxml/text_spec.rb +31 -0
  174. data/spec/moxml/version_spec.rb +14 -0
  175. data/spec/moxml/xml_utils/.gitkeep +0 -0
  176. data/spec/moxml/xml_utils/encoder_spec.rb +27 -0
  177. data/spec/moxml/xml_utils_spec.rb +49 -0
  178. data/spec/moxml/xpath/ast/node_spec.rb +83 -0
  179. data/spec/moxml/xpath/axes_spec.rb +296 -0
  180. data/spec/moxml/xpath/cache_spec.rb +358 -0
  181. data/spec/moxml/xpath/compiler_spec.rb +406 -0
  182. data/spec/moxml/xpath/context_spec.rb +210 -0
  183. data/spec/moxml/xpath/conversion_spec.rb +365 -0
  184. data/spec/moxml/xpath/fixtures/sample.xml +25 -0
  185. data/spec/moxml/xpath/functions/boolean_functions_spec.rb +114 -0
  186. data/spec/moxml/xpath/functions/node_functions_spec.rb +145 -0
  187. data/spec/moxml/xpath/functions/numeric_functions_spec.rb +164 -0
  188. data/spec/moxml/xpath/functions/position_functions_spec.rb +93 -0
  189. data/spec/moxml/xpath/functions/special_functions_spec.rb +89 -0
  190. data/spec/moxml/xpath/functions/string_functions_spec.rb +381 -0
  191. data/spec/moxml/xpath/lexer_spec.rb +488 -0
  192. data/spec/moxml/xpath/parser_integration_spec.rb +210 -0
  193. data/spec/moxml/xpath/parser_spec.rb +364 -0
  194. data/spec/moxml/xpath/ruby/generator_spec.rb +421 -0
  195. data/spec/moxml/xpath/ruby/node_spec.rb +291 -0
  196. data/spec/moxml/xpath_capabilities_spec.rb +199 -0
  197. data/spec/moxml/xpath_spec.rb +77 -0
  198. data/spec/performance/README.md +83 -0
  199. data/spec/performance/benchmark_spec.rb +64 -0
  200. data/spec/{support/shared_examples/examples/memory.rb → performance/memory_usage_spec.rb} +3 -1
  201. data/spec/{support/shared_examples/examples/thread_safety.rb → performance/thread_safety_spec.rb} +3 -1
  202. data/spec/performance/xpath_benchmark_spec.rb +259 -0
  203. data/spec/spec_helper.rb +58 -1
  204. data/spec/support/xml_matchers.rb +1 -1
  205. metadata +176 -34
  206. data/spec/support/shared_examples/examples/benchmark_spec.rb +0 -51
  207. /data/spec/{support/shared_examples/builder.rb → integration/shared_examples/high_level/builder_behavior.rb} +0 -0
  208. /data/spec/{support/shared_examples/document_builder.rb → integration/shared_examples/high_level/document_builder_behavior.rb} +0 -0
  209. /data/spec/{support/shared_examples/attribute.rb → integration/shared_examples/node_wrappers/attribute_behavior.rb} +0 -0
  210. /data/spec/{support/shared_examples/element.rb → integration/shared_examples/node_wrappers/element_behavior.rb} +0 -0
  211. /data/spec/{support/shared_examples/namespace.rb → integration/shared_examples/node_wrappers/namespace_behavior.rb} +0 -0
  212. /data/spec/{support/shared_examples/text.rb → integration/shared_examples/node_wrappers/text_behavior.rb} +0 -0
@@ -0,0 +1,365 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::XPath::Conversion do
6
+ let(:context) { Moxml.new }
7
+
8
+ describe ".to_string" do
9
+ it "converts integers to strings" do
10
+ expect(described_class.to_string(42)).to eq("42")
11
+ end
12
+
13
+ it "converts floats with zero decimal to integers first" do
14
+ expect(described_class.to_string(10.0)).to eq("10")
15
+ end
16
+
17
+ it "preserves non-zero decimals in floats" do
18
+ expect(described_class.to_string(10.5)).to eq("10.5")
19
+ end
20
+
21
+ it "converts strings to themselves" do
22
+ expect(described_class.to_string("hello")).to eq("hello")
23
+ end
24
+
25
+ it "converts true to string" do
26
+ expect(described_class.to_string(true)).to eq("true")
27
+ end
28
+
29
+ it "converts false to string" do
30
+ expect(described_class.to_string(false)).to eq("false")
31
+ end
32
+
33
+ it "converts nil to empty string" do
34
+ expect(described_class.to_string(nil)).to eq("")
35
+ end
36
+
37
+ context "with NodeSet" do
38
+ it "returns text of first node" do
39
+ doc = context.parse("<root><item>first</item><item>second</item></root>")
40
+ nodes = doc.xpath("//item")
41
+
42
+ expect(described_class.to_string(nodes)).to eq("first")
43
+ end
44
+
45
+ it "returns empty string for empty NodeSet" do
46
+ doc = context.parse("<root></root>")
47
+ nodes = doc.xpath("//item")
48
+
49
+ expect(described_class.to_string(nodes)).to eq("")
50
+ end
51
+ end
52
+
53
+ context "with nodes that respond to text" do
54
+ it "returns the text content of the node" do
55
+ doc = context.parse("<root>Hello World</root>")
56
+ node = doc.xpath("//root").first
57
+
58
+ expect(described_class.to_string(node)).to eq("Hello World")
59
+ end
60
+ end
61
+ end
62
+
63
+ describe ".to_float" do
64
+ it "converts integers to floats" do
65
+ expect(described_class.to_float(42)).to eq(42.0)
66
+ end
67
+
68
+ it "converts floats to themselves" do
69
+ expect(described_class.to_float(10.5)).to eq(10.5)
70
+ end
71
+
72
+ it "converts numeric strings to floats" do
73
+ expect(described_class.to_float("42.5")).to eq(42.5)
74
+ end
75
+
76
+ it "converts true to 1.0" do
77
+ expect(described_class.to_float(true)).to eq(1.0)
78
+ end
79
+
80
+ it "converts false to 0.0" do
81
+ expect(described_class.to_float(false)).to eq(0.0)
82
+ end
83
+
84
+ it "returns NaN for non-numeric strings" do
85
+ result = described_class.to_float("not a number")
86
+ expect(result).to be_nan
87
+ end
88
+
89
+ it "returns NaN for nil" do
90
+ result = described_class.to_float(nil)
91
+ expect(result).to be_nan
92
+ end
93
+
94
+ it "returns NaN for empty string" do
95
+ result = described_class.to_float("")
96
+ expect(result).to be_nan
97
+ end
98
+
99
+ context "with NodeSet" do
100
+ it "converts text of first node to float" do
101
+ doc = context.parse("<root><item>42.5</item></root>")
102
+ nodes = doc.xpath("//item")
103
+
104
+ expect(described_class.to_float(nodes)).to eq(42.5)
105
+ end
106
+
107
+ it "returns NaN for empty NodeSet" do
108
+ doc = context.parse("<root></root>")
109
+ nodes = doc.xpath("//item")
110
+
111
+ result = described_class.to_float(nodes)
112
+ expect(result).to be_nan
113
+ end
114
+
115
+ it "returns NaN for non-numeric node text" do
116
+ doc = context.parse("<root><item>hello</item></root>")
117
+ nodes = doc.xpath("//item")
118
+
119
+ result = described_class.to_float(nodes)
120
+ expect(result).to be_nan
121
+ end
122
+ end
123
+
124
+ context "with nodes that respond to text" do
125
+ it "converts node text to float" do
126
+ doc = context.parse("<root>123.45</root>")
127
+ node = doc.xpath("//root").first
128
+
129
+ expect(described_class.to_float(node)).to eq(123.45)
130
+ end
131
+ end
132
+ end
133
+
134
+ describe ".to_boolean" do
135
+ context "with floats" do
136
+ it "returns true for positive floats" do
137
+ expect(described_class.to_boolean(1.5)).to be(true)
138
+ end
139
+
140
+ it "returns false for zero" do
141
+ expect(described_class.to_boolean(0.0)).to be(false)
142
+ end
143
+
144
+ it "returns true for negative floats" do
145
+ expect(described_class.to_boolean(-1.5)).to be(true)
146
+ end
147
+
148
+ it "returns false for NaN" do
149
+ expect(described_class.to_boolean(Float::NAN)).to be(false)
150
+ end
151
+ end
152
+
153
+ context "with integers" do
154
+ it "returns true for positive integers" do
155
+ expect(described_class.to_boolean(42)).to be(true)
156
+ end
157
+
158
+ it "returns false for zero" do
159
+ expect(described_class.to_boolean(0)).to be(false)
160
+ end
161
+
162
+ it "returns true for negative integers" do
163
+ expect(described_class.to_boolean(-42)).to be(true)
164
+ end
165
+ end
166
+
167
+ context "with strings" do
168
+ it "returns true for non-empty strings" do
169
+ expect(described_class.to_boolean("hello")).to be(true)
170
+ end
171
+
172
+ it "returns false for empty strings" do
173
+ expect(described_class.to_boolean("")).to be(false)
174
+ end
175
+ end
176
+
177
+ context "with arrays" do
178
+ it "returns true for non-empty arrays" do
179
+ expect(described_class.to_boolean([1, 2, 3])).to be(true)
180
+ end
181
+
182
+ it "returns false for empty arrays" do
183
+ expect(described_class.to_boolean([])).to be(false)
184
+ end
185
+ end
186
+
187
+ context "with NodeSets" do
188
+ it "returns true for non-empty NodeSets" do
189
+ doc = context.parse("<root><item/></root>")
190
+ nodes = doc.xpath("//item")
191
+
192
+ expect(described_class.to_boolean(nodes)).to be(true)
193
+ end
194
+
195
+ it "returns false for empty NodeSets" do
196
+ doc = context.parse("<root></root>")
197
+ nodes = doc.xpath("//item")
198
+
199
+ expect(described_class.to_boolean(nodes)).to be(false)
200
+ end
201
+ end
202
+
203
+ context "with true/false" do
204
+ it "returns true for true" do
205
+ expect(described_class.to_boolean(true)).to be(true)
206
+ end
207
+
208
+ it "returns false for false" do
209
+ expect(described_class.to_boolean(false)).to be(false)
210
+ end
211
+ end
212
+
213
+ context "with nil" do
214
+ it "returns false for nil" do
215
+ expect(described_class.to_boolean(nil)).to be(false)
216
+ end
217
+ end
218
+
219
+ context "with other objects" do
220
+ it "returns true for any truthy object" do
221
+ expect(described_class.to_boolean(Object.new)).to be(true)
222
+ end
223
+ end
224
+ end
225
+
226
+ describe ".to_compatible_types" do
227
+ it "converts NodeSet to string when compared with another value" do
228
+ doc = context.parse("<root><item>hello</item></root>")
229
+ nodes = doc.xpath("//item")
230
+
231
+ left, right = described_class.to_compatible_types(nodes, "world")
232
+ expect(left).to eq("hello")
233
+ expect(right).to eq("world")
234
+ end
235
+
236
+ it "converts node to string when compared with another value" do
237
+ doc = context.parse("<root>test</root>")
238
+ node = doc.xpath("//root").first
239
+
240
+ left, right = described_class.to_compatible_types(node, "other")
241
+ expect(left).to eq("test")
242
+ expect(right).to eq("other")
243
+ end
244
+
245
+ it "converts right operand to float when left is numeric" do
246
+ left, right = described_class.to_compatible_types(42, "10")
247
+ expect(left).to eq(42)
248
+ expect(right).to eq(10.0)
249
+ end
250
+
251
+ it "converts right operand to string when left is string" do
252
+ left, right = described_class.to_compatible_types("hello", 42)
253
+ expect(left).to eq("hello")
254
+ expect(right).to eq("42")
255
+ end
256
+
257
+ it "converts right operand to boolean when left is boolean" do
258
+ left, right = described_class.to_compatible_types(true, "non-empty")
259
+ expect(left).to be(true)
260
+ expect(right).to be(true)
261
+ end
262
+
263
+ it "handles both NodeSets" do
264
+ doc = context.parse("<root><a>text1</a><b>text2</b></root>")
265
+ nodes1 = doc.xpath("//a")
266
+ nodes2 = doc.xpath("//b")
267
+
268
+ left, right = described_class.to_compatible_types(nodes1, nodes2)
269
+ expect(left).to eq("text1")
270
+ expect(right).to eq("text2")
271
+ end
272
+
273
+ it "preserves compatible types" do
274
+ left, right = described_class.to_compatible_types(10, 20)
275
+ expect(left).to eq(10)
276
+ expect(right).to eq(20)
277
+ end
278
+
279
+ it "handles false boolean correctly" do
280
+ left, right = described_class.to_compatible_types(false, "text")
281
+ expect(left).to be(false)
282
+ expect(right).to be(true)
283
+ end
284
+ end
285
+
286
+ describe ".boolean?" do
287
+ it "returns true for true" do
288
+ expect(described_class.boolean?(true)).to be(true)
289
+ end
290
+
291
+ it "returns true for false" do
292
+ expect(described_class.boolean?(false)).to be(true)
293
+ end
294
+
295
+ it "returns false for integers" do
296
+ expect(described_class.boolean?(42)).to be(false)
297
+ end
298
+
299
+ it "returns false for strings" do
300
+ expect(described_class.boolean?("true")).to be(false)
301
+ end
302
+
303
+ it "returns false for nil" do
304
+ expect(described_class.boolean?(nil)).to be(false)
305
+ end
306
+
307
+ it "returns false for arrays" do
308
+ expect(described_class.boolean?([true])).to be(false)
309
+ end
310
+ end
311
+
312
+ describe ".first_node_text" do
313
+ it "returns text of first node in NodeSet" do
314
+ doc = context.parse("<root><item>first</item><item>second</item></root>")
315
+ nodes = doc.xpath("//item")
316
+
317
+ expect(described_class.first_node_text(nodes)).to eq("first")
318
+ end
319
+
320
+ it "returns empty string if first node does not respond to text" do
321
+ # Create a mock NodeSet with a node that doesn't respond to text
322
+ node_set = [double("node")]
323
+ allow(node_set).to receive(:[]).with(0).and_return(node_set[0])
324
+
325
+ expect(described_class.first_node_text(node_set)).to eq("")
326
+ end
327
+
328
+ it "handles nodes with empty text" do
329
+ doc = context.parse("<root><item></item></root>")
330
+ nodes = doc.xpath("//item")
331
+
332
+ expect(described_class.first_node_text(nodes)).to eq("")
333
+ end
334
+
335
+ it "handles nodes with whitespace text" do
336
+ doc = context.parse("<root><item> </item></root>")
337
+ nodes = doc.xpath("//item")
338
+
339
+ expect(described_class.first_node_text(nodes)).to eq(" ")
340
+ end
341
+ end
342
+
343
+ describe "edge cases" do
344
+ it "handles Float::INFINITY in to_boolean" do
345
+ expect(described_class.to_boolean(Float::INFINITY)).to be(true)
346
+ end
347
+
348
+ it "handles -Float::INFINITY in to_boolean" do
349
+ expect(described_class.to_boolean(-Float::INFINITY)).to be(true)
350
+ end
351
+
352
+ it "handles scientific notation strings in to_float" do
353
+ expect(described_class.to_float("1.5e2")).to eq(150.0)
354
+ end
355
+
356
+ it "handles negative numbers in to_float" do
357
+ expect(described_class.to_float("-42.5")).to eq(-42.5)
358
+ end
359
+
360
+ it "handles very large numbers" do
361
+ large_num = 10**100
362
+ expect(described_class.to_string(large_num)).to eq(large_num.to_s)
363
+ end
364
+ end
365
+ end
@@ -0,0 +1,25 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <library>
3
+ <book id="1" category="fiction">
4
+ <title lang="en">The Great Gatsby</title>
5
+ <author>F. Scott Fitzgerald</author>
6
+ <year>1925</year>
7
+ <price currency="USD">10.99</price>
8
+ </book>
9
+ <book id="2" category="fiction">
10
+ <title lang="en">To Kill a Mockingbird</title>
11
+ <author>Harper Lee</author>
12
+ <year>1960</year>
13
+ <price currency="USD">12.99</price>
14
+ </book>
15
+ <book id="3" category="non-fiction">
16
+ <title lang="en">Sapiens</title>
17
+ <author>Yuval Noah Harari</author>
18
+ <year>2011</year>
19
+ <price currency="USD">15.99</price>
20
+ </book>
21
+ <magazine id="4">
22
+ <title>Scientific American</title>
23
+ <issue number="12">December 2023</issue>
24
+ </magazine>
25
+ </library>
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "XPath Boolean Functions" do
6
+ let(:doc) do
7
+ xml = "<root><item/></root>"
8
+ Moxml.new(:nokogiri).parse(xml)
9
+ end
10
+
11
+ describe "boolean()" do
12
+ it "converts non-empty string to true" do
13
+ ast = Moxml::XPath::Parser.parse('boolean("text")')
14
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
15
+ result = proc.call(doc)
16
+
17
+ expect(result).to be true
18
+ end
19
+
20
+ it "converts empty string to false" do
21
+ ast = Moxml::XPath::Parser.parse('boolean("")')
22
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
23
+ result = proc.call(doc)
24
+
25
+ expect(result).to be false
26
+ end
27
+
28
+ it "converts non-zero number to true" do
29
+ ast = Moxml::XPath::Parser.parse("boolean(1)")
30
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
31
+ result = proc.call(doc)
32
+
33
+ expect(result).to be true
34
+ end
35
+
36
+ it "converts zero to false" do
37
+ ast = Moxml::XPath::Parser.parse("boolean(0)")
38
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
39
+ result = proc.call(doc)
40
+
41
+ expect(result).to be false
42
+ end
43
+
44
+ it "converts NaN to false" do
45
+ ast = Moxml::XPath::Parser.parse('boolean(number("not-a-number"))')
46
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
47
+ result = proc.call(doc)
48
+
49
+ expect(result).to be false
50
+ end
51
+ end
52
+
53
+ describe "not()" do
54
+ it "negates true" do
55
+ ast = Moxml::XPath::Parser.parse("not(true())")
56
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
57
+ result = proc.call(doc)
58
+
59
+ expect(result).to be false
60
+ end
61
+
62
+ it "negates false" do
63
+ ast = Moxml::XPath::Parser.parse("not(false())")
64
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
65
+ result = proc.call(doc)
66
+
67
+ expect(result).to be true
68
+ end
69
+
70
+ it "negates non-empty string" do
71
+ ast = Moxml::XPath::Parser.parse('not("text")')
72
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
73
+ result = proc.call(doc)
74
+
75
+ expect(result).to be false
76
+ end
77
+
78
+ it "negates empty string" do
79
+ ast = Moxml::XPath::Parser.parse('not("")')
80
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
81
+ result = proc.call(doc)
82
+
83
+ expect(result).to be true
84
+ end
85
+
86
+ it "negates number" do
87
+ ast = Moxml::XPath::Parser.parse("not(0)")
88
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
89
+ result = proc.call(doc)
90
+
91
+ expect(result).to be true
92
+ end
93
+ end
94
+
95
+ describe "true()" do
96
+ it "returns true" do
97
+ ast = Moxml::XPath::Parser.parse("true()")
98
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
99
+ result = proc.call(doc)
100
+
101
+ expect(result).to be true
102
+ end
103
+ end
104
+
105
+ describe "false()" do
106
+ it "returns false" do
107
+ ast = Moxml::XPath::Parser.parse("false()")
108
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
109
+ result = proc.call(doc)
110
+
111
+ expect(result).to be false
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "XPath Node Functions" do
6
+ let(:context) { Moxml.new(:nokogiri) }
7
+
8
+ let(:doc) do
9
+ xml = <<~XML
10
+ <root xmlns:ns="http://example.com">
11
+ <ns:item>namespaced</ns:item>
12
+ <child id="123">content</child>
13
+ </root>
14
+ XML
15
+ context.parse(xml)
16
+ end
17
+
18
+ describe "local-name()" do
19
+ it "returns local name without namespace prefix" do
20
+ ast = Moxml::XPath::Parser.parse("local-name(/root/child)")
21
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
22
+ result = proc.call(doc)
23
+
24
+ expect(result).to eq("child")
25
+ end
26
+
27
+ it "returns local name for namespaced element" do
28
+ ast = Moxml::XPath::Parser.parse("local-name(/root/*[1])")
29
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
30
+ result = proc.call(doc)
31
+
32
+ expect(result).to eq("item")
33
+ end
34
+
35
+ it "returns empty string for no argument on non-element" do
36
+ ast = Moxml::XPath::Parser.parse("local-name()")
37
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
38
+ root = doc.root
39
+ result = proc.call(root)
40
+
41
+ expect(result).to eq("root")
42
+ end
43
+ end
44
+
45
+ describe "name()" do
46
+ it "returns qualified name for element" do
47
+ ast = Moxml::XPath::Parser.parse("name(/root/child)")
48
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
49
+ result = proc.call(doc)
50
+
51
+ expect(result).to eq("child")
52
+ end
53
+
54
+ it "returns qualified name with namespace prefix" do
55
+ ast = Moxml::XPath::Parser.parse("name(/root/*[1])")
56
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
57
+ result = proc.call(doc)
58
+
59
+ # Depending on adapter, may include ns: prefix
60
+ expect(result).to match(/item/)
61
+ end
62
+
63
+ it "returns empty string when no node matched" do
64
+ ast = Moxml::XPath::Parser.parse("name(/nonexistent)")
65
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
66
+ result = proc.call(doc)
67
+
68
+ expect(result).to eq("")
69
+ end
70
+ end
71
+
72
+ describe "namespace-uri()" do
73
+ it "returns empty string for element without namespace" do
74
+ ast = Moxml::XPath::Parser.parse("namespace-uri(/root/child)")
75
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
76
+ result = proc.call(doc)
77
+
78
+ expect(result).to eq("")
79
+ end
80
+
81
+ it "returns namespace URI for namespaced element" do
82
+ ast = Moxml::XPath::Parser.parse("namespace-uri(/root/*[1])")
83
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
84
+ result = proc.call(doc)
85
+
86
+ expect(result).to eq("http://example.com")
87
+ end
88
+
89
+ it "returns empty string when no node matched" do
90
+ ast = Moxml::XPath::Parser.parse("namespace-uri(/nonexistent)")
91
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
92
+ result = proc.call(doc)
93
+
94
+ expect(result).to eq("")
95
+ end
96
+ end
97
+
98
+ describe "lang()" do
99
+ let(:doc_with_lang) do
100
+ xml = <<~XML
101
+ <root xml:lang="en">
102
+ <child>text</child>
103
+ <other xml:lang="fr">french</other>
104
+ </root>
105
+ XML
106
+ context.parse(xml)
107
+ end
108
+
109
+ it "matches language on element with xml:lang" do
110
+ ast = Moxml::XPath::Parser.parse('lang("en")')
111
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
112
+ root = doc_with_lang.root
113
+ result = proc.call(root)
114
+
115
+ expect(result).to be true
116
+ end
117
+
118
+ it "does not match wrong language" do
119
+ ast = Moxml::XPath::Parser.parse('lang("fr")')
120
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
121
+ root = doc_with_lang.root
122
+ result = proc.call(root)
123
+
124
+ expect(result).to be false
125
+ end
126
+
127
+ it "inherits language from parent element" do
128
+ ast = Moxml::XPath::Parser.parse('lang("en")')
129
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
130
+ child = doc_with_lang.root.children.first
131
+ result = proc.call(child)
132
+
133
+ expect(result).to be true
134
+ end
135
+
136
+ it "uses closest xml:lang attribute" do
137
+ ast = Moxml::XPath::Parser.parse('lang("fr")')
138
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
139
+ other = doc_with_lang.root.children[1]
140
+ result = proc.call(other)
141
+
142
+ expect(result).to be true
143
+ end
144
+ end
145
+ end