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.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-repos.json +5 -0
- data/.github/workflows/dependent-tests.yml +20 -0
- data/.github/workflows/docs.yml +59 -0
- data/.github/workflows/rake.yml +10 -10
- data/.github/workflows/release.yml +5 -3
- data/.gitignore +37 -0
- data/.rubocop.yml +15 -7
- data/.rubocop_todo.yml +224 -43
- data/Gemfile +14 -9
- data/LICENSE.md +6 -2
- data/README.adoc +535 -373
- data/Rakefile +53 -0
- data/benchmarks/.gitignore +6 -0
- data/benchmarks/generate_report.rb +550 -0
- data/docs/Gemfile +13 -0
- data/docs/_config.yml +138 -0
- data/docs/_guides/advanced-features.adoc +87 -0
- data/docs/_guides/development-testing.adoc +165 -0
- data/docs/_guides/index.adoc +51 -0
- data/docs/_guides/modifying-xml.adoc +292 -0
- data/docs/_guides/parsing-xml.adoc +230 -0
- data/docs/_guides/sax-parsing.adoc +603 -0
- data/docs/_guides/working-with-documents.adoc +118 -0
- data/docs/_guides/xml-declaration.adoc +450 -0
- data/docs/_pages/adapter-compatibility.adoc +369 -0
- data/docs/_pages/adapters/headed-ox.adoc +237 -0
- data/docs/_pages/adapters/index.adoc +97 -0
- data/docs/_pages/adapters/libxml.adoc +285 -0
- data/docs/_pages/adapters/nokogiri.adoc +251 -0
- data/docs/_pages/adapters/oga.adoc +291 -0
- data/docs/_pages/adapters/ox.adoc +56 -0
- data/docs/_pages/adapters/rexml.adoc +292 -0
- data/docs/_pages/best-practices.adoc +429 -0
- data/docs/_pages/compatibility.adoc +467 -0
- data/docs/_pages/configuration.adoc +250 -0
- data/docs/_pages/error-handling.adoc +349 -0
- data/docs/_pages/headed-ox-limitations.adoc +574 -0
- data/docs/_pages/headed-ox.adoc +1025 -0
- data/docs/_pages/index.adoc +35 -0
- data/docs/_pages/installation.adoc +140 -0
- data/docs/_pages/node-api-reference.adoc +49 -0
- data/docs/_pages/performance.adoc +35 -0
- data/docs/_pages/quick-start.adoc +243 -0
- data/docs/_pages/thread-safety.adoc +28 -0
- data/docs/_references/document-api.adoc +407 -0
- data/docs/_references/index.adoc +48 -0
- data/docs/_tutorials/basic-usage.adoc +267 -0
- data/docs/_tutorials/builder-pattern.adoc +342 -0
- data/docs/_tutorials/index.adoc +33 -0
- data/docs/_tutorials/namespace-handling.adoc +324 -0
- data/docs/_tutorials/xpath-queries.adoc +358 -0
- data/docs/index.adoc +122 -0
- data/examples/README.md +124 -0
- data/examples/api_client/README.md +424 -0
- data/examples/api_client/api_client.rb +394 -0
- data/examples/api_client/example_response.xml +48 -0
- data/examples/headed_ox_example/README.md +90 -0
- data/examples/headed_ox_example/headed_ox_demo.rb +71 -0
- data/examples/rss_parser/README.md +194 -0
- data/examples/rss_parser/example_feed.xml +93 -0
- data/examples/rss_parser/rss_parser.rb +189 -0
- data/examples/sax_parsing/README.md +50 -0
- data/examples/sax_parsing/data_extractor.rb +75 -0
- data/examples/sax_parsing/example.xml +21 -0
- data/examples/sax_parsing/large_file.rb +78 -0
- data/examples/sax_parsing/simple_parser.rb +55 -0
- data/examples/web_scraper/README.md +352 -0
- data/examples/web_scraper/example_page.html +201 -0
- data/examples/web_scraper/web_scraper.rb +312 -0
- data/lib/moxml/adapter/base.rb +107 -28
- data/lib/moxml/adapter/customized_libxml/cdata.rb +28 -0
- data/lib/moxml/adapter/customized_libxml/comment.rb +24 -0
- data/lib/moxml/adapter/customized_libxml/declaration.rb +85 -0
- data/lib/moxml/adapter/customized_libxml/element.rb +39 -0
- data/lib/moxml/adapter/customized_libxml/node.rb +44 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +31 -0
- data/lib/moxml/adapter/customized_libxml/text.rb +27 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +1 -1
- data/lib/moxml/adapter/customized_ox/attribute.rb +28 -1
- data/lib/moxml/adapter/customized_rexml/formatter.rb +13 -8
- data/lib/moxml/adapter/headed_ox.rb +161 -0
- data/lib/moxml/adapter/libxml.rb +1564 -0
- data/lib/moxml/adapter/nokogiri.rb +156 -9
- data/lib/moxml/adapter/oga.rb +190 -15
- data/lib/moxml/adapter/ox.rb +322 -28
- data/lib/moxml/adapter/rexml.rb +157 -28
- data/lib/moxml/adapter.rb +21 -4
- data/lib/moxml/attribute.rb +6 -0
- data/lib/moxml/builder.rb +40 -4
- data/lib/moxml/config.rb +8 -3
- data/lib/moxml/context.rb +57 -2
- data/lib/moxml/declaration.rb +9 -0
- data/lib/moxml/doctype.rb +13 -1
- data/lib/moxml/document.rb +53 -6
- data/lib/moxml/document_builder.rb +34 -5
- data/lib/moxml/element.rb +71 -2
- data/lib/moxml/error.rb +175 -6
- data/lib/moxml/node.rb +155 -4
- data/lib/moxml/node_set.rb +34 -0
- data/lib/moxml/sax/block_handler.rb +194 -0
- data/lib/moxml/sax/element_handler.rb +124 -0
- data/lib/moxml/sax/handler.rb +113 -0
- data/lib/moxml/sax.rb +31 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_utils/encoder.rb +4 -4
- data/lib/moxml/xml_utils.rb +7 -4
- data/lib/moxml/xpath/ast/node.rb +159 -0
- data/lib/moxml/xpath/cache.rb +91 -0
- data/lib/moxml/xpath/compiler.rb +1770 -0
- data/lib/moxml/xpath/context.rb +26 -0
- data/lib/moxml/xpath/conversion.rb +124 -0
- data/lib/moxml/xpath/engine.rb +52 -0
- data/lib/moxml/xpath/errors.rb +101 -0
- data/lib/moxml/xpath/lexer.rb +304 -0
- data/lib/moxml/xpath/parser.rb +485 -0
- data/lib/moxml/xpath/ruby/generator.rb +269 -0
- data/lib/moxml/xpath/ruby/node.rb +193 -0
- data/lib/moxml/xpath.rb +37 -0
- data/lib/moxml.rb +5 -2
- data/moxml.gemspec +3 -1
- data/old-specs/moxml/adapter/customized_libxml/.gitkeep +6 -0
- data/spec/consistency/README.md +77 -0
- data/spec/{moxml/examples/adapter_spec.rb → consistency/adapter_parity_spec.rb} +4 -4
- data/spec/examples/README.md +75 -0
- data/spec/{support/shared_examples/examples/attribute.rb → examples/attribute_examples_spec.rb} +1 -1
- data/spec/{support/shared_examples/examples/basic_usage.rb → examples/basic_usage_spec.rb} +2 -2
- data/spec/{support/shared_examples/examples/namespace.rb → examples/namespace_examples_spec.rb} +3 -3
- data/spec/{support/shared_examples/examples/readme_examples.rb → examples/readme_examples_spec.rb} +6 -4
- data/spec/{support/shared_examples/examples/xpath.rb → examples/xpath_examples_spec.rb} +10 -6
- data/spec/integration/README.md +71 -0
- data/spec/{moxml/all_with_adapters_spec.rb → integration/all_adapters_spec.rb} +3 -2
- data/spec/integration/headed_ox_integration_spec.rb +326 -0
- data/spec/{support → integration}/shared_examples/edge_cases.rb +37 -10
- data/spec/integration/shared_examples/high_level/.gitkeep +0 -0
- data/spec/{support/shared_examples/context.rb → integration/shared_examples/high_level/context_behavior.rb} +2 -1
- data/spec/{support/shared_examples/integration.rb → integration/shared_examples/integration_workflows.rb} +23 -6
- data/spec/integration/shared_examples/node_wrappers/.gitkeep +0 -0
- data/spec/{support/shared_examples/cdata.rb → integration/shared_examples/node_wrappers/cdata_behavior.rb} +6 -1
- data/spec/{support/shared_examples/comment.rb → integration/shared_examples/node_wrappers/comment_behavior.rb} +2 -1
- data/spec/{support/shared_examples/declaration.rb → integration/shared_examples/node_wrappers/declaration_behavior.rb} +5 -5
- data/spec/{support/shared_examples/doctype.rb → integration/shared_examples/node_wrappers/doctype_behavior.rb} +2 -2
- data/spec/{support/shared_examples/document.rb → integration/shared_examples/node_wrappers/document_behavior.rb} +1 -1
- data/spec/{support/shared_examples/node.rb → integration/shared_examples/node_wrappers/node_behavior.rb} +9 -2
- data/spec/{support/shared_examples/node_set.rb → integration/shared_examples/node_wrappers/node_set_behavior.rb} +1 -18
- data/spec/{support/shared_examples/processing_instruction.rb → integration/shared_examples/node_wrappers/processing_instruction_behavior.rb} +6 -2
- data/spec/moxml/README.md +41 -0
- data/spec/moxml/adapter/.gitkeep +0 -0
- data/spec/moxml/adapter/README.md +61 -0
- data/spec/moxml/adapter/base_spec.rb +27 -0
- data/spec/moxml/adapter/headed_ox_spec.rb +311 -0
- data/spec/moxml/adapter/libxml_spec.rb +14 -0
- data/spec/moxml/adapter/ox_spec.rb +9 -8
- data/spec/moxml/adapter/shared_examples/.gitkeep +0 -0
- data/spec/{support/shared_examples/xml_adapter.rb → moxml/adapter/shared_examples/adapter_contract.rb} +39 -12
- data/spec/moxml/adapter_spec.rb +16 -0
- data/spec/moxml/attribute_spec.rb +30 -0
- data/spec/moxml/builder_spec.rb +33 -0
- data/spec/moxml/cdata_spec.rb +31 -0
- data/spec/moxml/comment_spec.rb +31 -0
- data/spec/moxml/config_spec.rb +3 -3
- data/spec/moxml/context_spec.rb +28 -0
- data/spec/moxml/declaration_preservation_spec.rb +217 -0
- data/spec/moxml/declaration_spec.rb +36 -0
- data/spec/moxml/doctype_spec.rb +33 -0
- data/spec/moxml/document_builder_spec.rb +30 -0
- data/spec/moxml/document_spec.rb +105 -0
- data/spec/moxml/element_spec.rb +143 -0
- data/spec/moxml/error_spec.rb +266 -22
- data/spec/{moxml_spec.rb → moxml/moxml_spec.rb} +9 -9
- data/spec/moxml/namespace_spec.rb +32 -0
- data/spec/moxml/node_set_spec.rb +39 -0
- data/spec/moxml/node_spec.rb +37 -0
- data/spec/moxml/processing_instruction_spec.rb +34 -0
- data/spec/moxml/sax_spec.rb +1067 -0
- data/spec/moxml/text_spec.rb +31 -0
- data/spec/moxml/version_spec.rb +14 -0
- data/spec/moxml/xml_utils/.gitkeep +0 -0
- data/spec/moxml/xml_utils/encoder_spec.rb +27 -0
- data/spec/moxml/xml_utils_spec.rb +49 -0
- data/spec/moxml/xpath/ast/node_spec.rb +83 -0
- data/spec/moxml/xpath/axes_spec.rb +296 -0
- data/spec/moxml/xpath/cache_spec.rb +358 -0
- data/spec/moxml/xpath/compiler_spec.rb +406 -0
- data/spec/moxml/xpath/context_spec.rb +210 -0
- data/spec/moxml/xpath/conversion_spec.rb +365 -0
- data/spec/moxml/xpath/fixtures/sample.xml +25 -0
- data/spec/moxml/xpath/functions/boolean_functions_spec.rb +114 -0
- data/spec/moxml/xpath/functions/node_functions_spec.rb +145 -0
- data/spec/moxml/xpath/functions/numeric_functions_spec.rb +164 -0
- data/spec/moxml/xpath/functions/position_functions_spec.rb +93 -0
- data/spec/moxml/xpath/functions/special_functions_spec.rb +89 -0
- data/spec/moxml/xpath/functions/string_functions_spec.rb +381 -0
- data/spec/moxml/xpath/lexer_spec.rb +488 -0
- data/spec/moxml/xpath/parser_integration_spec.rb +210 -0
- data/spec/moxml/xpath/parser_spec.rb +364 -0
- data/spec/moxml/xpath/ruby/generator_spec.rb +421 -0
- data/spec/moxml/xpath/ruby/node_spec.rb +291 -0
- data/spec/moxml/xpath_capabilities_spec.rb +199 -0
- data/spec/moxml/xpath_spec.rb +77 -0
- data/spec/performance/README.md +83 -0
- data/spec/performance/benchmark_spec.rb +64 -0
- data/spec/{support/shared_examples/examples/memory.rb → performance/memory_usage_spec.rb} +4 -1
- data/spec/{support/shared_examples/examples/thread_safety.rb → performance/thread_safety_spec.rb} +3 -1
- data/spec/performance/xpath_benchmark_spec.rb +259 -0
- data/spec/spec_helper.rb +58 -1
- data/spec/support/xml_matchers.rb +1 -1
- metadata +178 -34
- data/spec/support/shared_examples/examples/benchmark_spec.rb +0 -51
- /data/spec/{support/shared_examples/builder.rb → integration/shared_examples/high_level/builder_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/document_builder.rb → integration/shared_examples/high_level/document_builder_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/attribute.rb → integration/shared_examples/node_wrappers/attribute_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/element.rb → integration/shared_examples/node_wrappers/element_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/namespace.rb → integration/shared_examples/node_wrappers/namespace_behavior.rb} +0 -0
- /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
|