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,381 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "XPath String Functions" do
6
+ let(:doc) do
7
+ xml = <<~XML
8
+ <root>
9
+ <item>Hello World</item>
10
+ <item> Spaces Around </item>
11
+ <item>UPPERCASE</item>
12
+ <book id="1">Programming Ruby</book>
13
+ <book id="2">Learning XML</book>
14
+ </root>
15
+ XML
16
+ Moxml.new(:nokogiri).parse(xml)
17
+ end
18
+
19
+ describe "string()" do
20
+ it "converts number to string" do
21
+ ast = Moxml::XPath::Parser.parse("string(123)")
22
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
23
+ result = proc.call(doc)
24
+
25
+ expect(result).to eq("123")
26
+ end
27
+
28
+ it "converts float to string" do
29
+ ast = Moxml::XPath::Parser.parse("string(123.45)")
30
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
31
+ result = proc.call(doc)
32
+
33
+ expect(result).to eq("123.45")
34
+ end
35
+
36
+ it "converts node to string" do
37
+ ast = Moxml::XPath::Parser.parse("string(/root/item[1])")
38
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
39
+ result = proc.call(doc)
40
+
41
+ expect(result).to eq("Hello World")
42
+ end
43
+
44
+ it "returns empty string for non-existent node" do
45
+ ast = Moxml::XPath::Parser.parse("string(/root/nonexistent)")
46
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
47
+ result = proc.call(doc)
48
+
49
+ expect(result).to eq("")
50
+ end
51
+ end
52
+
53
+ describe "concat()" do
54
+ it "concatenates two strings" do
55
+ ast = Moxml::XPath::Parser.parse('concat("Hello", " World")')
56
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
57
+ result = proc.call(doc)
58
+
59
+ expect(result).to eq("Hello World")
60
+ end
61
+
62
+ it "concatenates multiple strings" do
63
+ ast = Moxml::XPath::Parser.parse('concat("a", "b", "c", "d")')
64
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
65
+ result = proc.call(doc)
66
+
67
+ expect(result).to eq("abcd")
68
+ end
69
+
70
+ it "concatenates mixed literals and node values" do
71
+ ast = Moxml::XPath::Parser.parse('concat("Book: ", /root/book[1])')
72
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
73
+ result = proc.call(doc)
74
+
75
+ expect(result).to eq("Book: Programming Ruby")
76
+ end
77
+ end
78
+
79
+ describe "starts-with()" do
80
+ it "returns true for matching prefix" do
81
+ ast = Moxml::XPath::Parser.parse('starts-with("Hello World", "Hello")')
82
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
83
+ result = proc.call(doc)
84
+
85
+ expect(result).to be true
86
+ end
87
+
88
+ it "returns false for non-matching prefix" do
89
+ ast = Moxml::XPath::Parser.parse('starts-with("Hello World", "World")')
90
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
91
+ result = proc.call(doc)
92
+
93
+ expect(result).to be false
94
+ end
95
+
96
+ it "returns true for empty prefix" do
97
+ ast = Moxml::XPath::Parser.parse('starts-with("Hello", "")')
98
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
99
+ result = proc.call(doc)
100
+
101
+ expect(result).to be true
102
+ end
103
+
104
+ it "works with node values" do
105
+ ast = Moxml::XPath::Parser.parse('starts-with(/root/item[1], "Hello")')
106
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
107
+ result = proc.call(doc)
108
+
109
+ expect(result).to be true
110
+ end
111
+ end
112
+
113
+ describe "contains()" do
114
+ it "returns true when substring is present" do
115
+ ast = Moxml::XPath::Parser.parse('contains("Hello World", "Wor")')
116
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
117
+ result = proc.call(doc)
118
+
119
+ expect(result).to be true
120
+ end
121
+
122
+ it "returns false when substring is not present" do
123
+ ast = Moxml::XPath::Parser.parse('contains("Hello World", "xyz")')
124
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
125
+ result = proc.call(doc)
126
+
127
+ expect(result).to be false
128
+ end
129
+
130
+ it "is case-sensitive" do
131
+ ast = Moxml::XPath::Parser.parse('contains("Hello World", "hello")')
132
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
133
+ result = proc.call(doc)
134
+
135
+ expect(result).to be false
136
+ end
137
+
138
+ it "works with node values" do
139
+ ast = Moxml::XPath::Parser.parse('contains(/root/item[1], "World")')
140
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
141
+ result = proc.call(doc)
142
+
143
+ expect(result).to be true
144
+ end
145
+ end
146
+
147
+ describe "substring-before()" do
148
+ it "returns text before separator" do
149
+ ast = Moxml::XPath::Parser.parse('substring-before("Hello:World", ":")')
150
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
151
+ result = proc.call(doc)
152
+
153
+ expect(result).to eq("Hello")
154
+ end
155
+
156
+ it "returns empty string when separator is not found" do
157
+ ast = Moxml::XPath::Parser.parse('substring-before("Hello World", ":")')
158
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
159
+ result = proc.call(doc)
160
+
161
+ expect(result).to eq("")
162
+ end
163
+
164
+ it "works with multi-character separator" do
165
+ ast = Moxml::XPath::Parser.parse('substring-before("Hello::World", "::")')
166
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
167
+ result = proc.call(doc)
168
+
169
+ expect(result).to eq("Hello")
170
+ end
171
+ end
172
+
173
+ describe "substring-after()" do
174
+ it "returns text after separator" do
175
+ ast = Moxml::XPath::Parser.parse('substring-after("Hello:World", ":")')
176
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
177
+ result = proc.call(doc)
178
+
179
+ expect(result).to eq("World")
180
+ end
181
+
182
+ it "returns empty string when separator is not found" do
183
+ ast = Moxml::XPath::Parser.parse('substring-after("Hello World", ":")')
184
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
185
+ result = proc.call(doc)
186
+
187
+ expect(result).to eq("")
188
+ end
189
+
190
+ it "works with multi-character separator" do
191
+ ast = Moxml::XPath::Parser.parse('substring-after("Hello::World", "::")')
192
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
193
+ result = proc.call(doc)
194
+
195
+ expect(result).to eq("World")
196
+ end
197
+ end
198
+
199
+ describe "substring()" do
200
+ it "extracts substring with start and length" do
201
+ ast = Moxml::XPath::Parser.parse('substring("Hello World", 1, 5)')
202
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
203
+ result = proc.call(doc)
204
+
205
+ expect(result).to eq("Hello")
206
+ end
207
+
208
+ it "extracts substring with only start position" do
209
+ ast = Moxml::XPath::Parser.parse('substring("Hello World", 7)')
210
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
211
+ result = proc.call(doc)
212
+
213
+ expect(result).to eq("World")
214
+ end
215
+
216
+ it "handles start position in middle of string" do
217
+ ast = Moxml::XPath::Parser.parse('substring("Hello World", 3, 3)')
218
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
219
+ result = proc.call(doc)
220
+
221
+ expect(result).to eq("llo")
222
+ end
223
+
224
+ it "returns empty string when start is beyond string length" do
225
+ ast = Moxml::XPath::Parser.parse('substring("Hello", 20, 5)')
226
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
227
+ result = proc.call(doc)
228
+
229
+ expect(result).to eq("")
230
+ end
231
+ end
232
+
233
+ describe "string-length()" do
234
+ it "returns length of string literal" do
235
+ ast = Moxml::XPath::Parser.parse('string-length("Hello")')
236
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
237
+ result = proc.call(doc)
238
+
239
+ expect(result).to eq(5.0)
240
+ end
241
+
242
+ it "returns zero for empty string" do
243
+ ast = Moxml::XPath::Parser.parse('string-length("")')
244
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
245
+ result = proc.call(doc)
246
+
247
+ expect(result).to eq(0.0)
248
+ end
249
+
250
+ it "works with node values" do
251
+ ast = Moxml::XPath::Parser.parse("string-length(/root/item[1])")
252
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
253
+ result = proc.call(doc)
254
+
255
+ expect(result).to eq(11.0)
256
+ end
257
+
258
+ it "counts multi-byte characters correctly" do
259
+ ast = Moxml::XPath::Parser.parse('string-length("Hello世界")')
260
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
261
+ result = proc.call(doc)
262
+
263
+ expect(result).to eq(7.0)
264
+ end
265
+ end
266
+
267
+ describe "normalize-space()" do
268
+ it "trims leading and trailing whitespace" do
269
+ ast = Moxml::XPath::Parser.parse('normalize-space(" Hello ")')
270
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
271
+ result = proc.call(doc)
272
+
273
+ expect(result).to eq("Hello")
274
+ end
275
+
276
+ it "collapses multiple spaces to single space" do
277
+ ast = Moxml::XPath::Parser.parse('normalize-space("Hello World")')
278
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
279
+ result = proc.call(doc)
280
+
281
+ expect(result).to eq("Hello World")
282
+ end
283
+
284
+ it "handles mixed whitespace characters" do
285
+ ast = Moxml::XPath::Parser.parse('normalize-space(" Hello \t\n World ")')
286
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
287
+ result = proc.call(doc)
288
+
289
+ expect(result).to eq("Hello World")
290
+ end
291
+
292
+ it "works with node values" do
293
+ ast = Moxml::XPath::Parser.parse("normalize-space(/root/item[2])")
294
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
295
+ result = proc.call(doc)
296
+
297
+ expect(result).to eq("Spaces Around")
298
+ end
299
+
300
+ it "returns empty string for whitespace-only input" do
301
+ ast = Moxml::XPath::Parser.parse('normalize-space(" ")')
302
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
303
+ result = proc.call(doc)
304
+
305
+ expect(result).to eq("")
306
+ end
307
+ end
308
+
309
+ describe "translate()" do
310
+ it "translates single characters" do
311
+ ast = Moxml::XPath::Parser.parse('translate("abc", "abc", "123")')
312
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
313
+ result = proc.call(doc)
314
+
315
+ expect(result).to eq("123")
316
+ end
317
+
318
+ it "removes characters when replacement is shorter" do
319
+ ast = Moxml::XPath::Parser.parse('translate("abc", "abc", "12")')
320
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
321
+ result = proc.call(doc)
322
+
323
+ expect(result).to eq("12")
324
+ end
325
+
326
+ it "ignores extra replacement characters" do
327
+ ast = Moxml::XPath::Parser.parse('translate("ab", "ab", "1234")')
328
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
329
+ result = proc.call(doc)
330
+
331
+ expect(result).to eq("12")
332
+ end
333
+
334
+ it "preserves untranslated characters" do
335
+ ast = Moxml::XPath::Parser.parse('translate("hello", "el", "34")')
336
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
337
+ result = proc.call(doc)
338
+
339
+ expect(result).to eq("h344o")
340
+ end
341
+
342
+ it "works with case conversion" do
343
+ ast = Moxml::XPath::Parser.parse('translate("hello", "helo", "HELO")')
344
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
345
+ result = proc.call(doc)
346
+
347
+ expect(result).to eq("HELLO")
348
+ end
349
+ end
350
+
351
+ describe "function usage in predicates" do
352
+ it "filters with contains()" do
353
+ ast = Moxml::XPath::Parser.parse('//item[contains(., "Hello")]')
354
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
355
+ result = proc.call(doc)
356
+
357
+ expect(result).to be_a(Moxml::NodeSet)
358
+ expect(result.size).to eq(1)
359
+ expect(result.first.text).to eq("Hello World")
360
+ end
361
+
362
+ it "filters with starts-with()" do
363
+ ast = Moxml::XPath::Parser.parse('//book[starts-with(., "Programming")]')
364
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
365
+ result = proc.call(doc)
366
+
367
+ expect(result).to be_a(Moxml::NodeSet)
368
+ expect(result.size).to eq(1)
369
+ expect(result.first.text).to eq("Programming Ruby")
370
+ end
371
+
372
+ it "filters with string-length()" do
373
+ ast = Moxml::XPath::Parser.parse("//item[string-length(.) > 10]")
374
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
375
+ result = proc.call(doc)
376
+
377
+ expect(result).to be_a(Moxml::NodeSet)
378
+ expect(result.size).to eq(2)
379
+ end
380
+ end
381
+ end