xpath 2.0.0 → 3.2.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a243005210afa835c320ea204ab3105d0b253dfdb905d4ff53c8267e53a23d6d
4
+ data.tar.gz: 4978108857ddbd50e336926db82b6bb056656861572aa518654cff2dcadb2bc7
5
+ SHA512:
6
+ metadata.gz: e17ddd74ff29ac77050bec7cb5ee93f8b1c2386c56ea4390272d998540a69cf8f26f61541de3a583866b4531836d051e722ca93eabc9d52636e722aa78de491b
7
+ data.tar.gz: c75b155e85156faafa9e3ac63e7c16b7fbc20385b80ada25dcb5c0c680ea06fd06490a5d2e9096d68a51b22b62b06f5ad6b5d7b6e0191396e748bbd7e415d3ce
data/README.md CHANGED
@@ -3,7 +3,8 @@
3
3
  XPath is a Ruby DSL around a subset of XPath 1.0. Its primary purpose is to
4
4
  facilitate writing complex XPath queries from Ruby code.
5
5
 
6
- [![Build Status](https://secure.travis-ci.org/jnicklas/xpath.png?branch=master)](http://travis-ci.org/jnicklas/xpath)
6
+ [![Gem Version](https://badge.fury.io/rb/xpath.png)](http://badge.fury.io/rb/xpath)
7
+ [![Build Status](https://secure.travis-ci.org/teamcapybara/xpath.png?branch=master)](http://travis-ci.org/teamcapybara/xpath)
7
8
 
8
9
  ## Generating expressions
9
10
 
@@ -36,11 +37,9 @@ module MyXPaths
36
37
  end
37
38
  ```
38
39
 
39
- Both ways return an
40
- [`XPath::Expression`](http://rdoc.info/github/jnicklas/xpath/XPath/Expression)
41
- instance, which can be further modified. To convert the expression to a
42
- string, just call `#to_s` on it. All available expressions are defined in
43
- [`XPath::DSL`](http://rdoc.info/github/jnicklas/xpath/XPath/DSL).
40
+ Both ways return an `XPath::Expression` instance, which can be further
41
+ modified. To convert the expression to a string, just call `#to_s` on it. All
42
+ available expressions are defined in `XPath::DSL`.
44
43
 
45
44
  ## String, Hashes and Symbols
46
45
 
@@ -74,63 +73,6 @@ XPath.descendant(:p)[1]
74
73
  Keep in mind that XPath is 1-indexed and not 0-indexed like most other
75
74
  programming languages, including Ruby.
76
75
 
77
- Hashes are automatically converted to equality expressions, so the above
78
- example could be written as:
79
-
80
- ``` ruby
81
- XPath.descendant(:p)[:@id => 'foo']
82
- ```
83
-
84
- Which would generate the same expression:
85
-
86
- ```
87
- .//p[@id = 'foo']
88
- ```
89
-
90
- Note that the same rules apply here, both the keys and values in the hash are
91
- treated the same way as any other expression in XPath. Thus the following are
92
- not equivalent:
93
-
94
- ``` ruby
95
- XPath.descendant(:p)[:@id => 'foo'] # => .//p[@id = 'foo']
96
- XPath.descendant(:p)[:id => 'foo'] # => .//p[id = 'foo']
97
- XPath.descendant(:p)['id' => 'foo'] # => .//p['id' = 'foo']
98
- ```
99
-
100
- ## HTML
101
-
102
- XPath comes with a set of premade XPaths for use with HTML documents.
103
-
104
- You can generate these like this:
105
-
106
- ``` ruby
107
- XPath::HTML.link('Home')
108
- XPath::HTML.field('Name')
109
- ```
110
-
111
- See [`XPath::HTML`](http://rdoc.info/github/jnicklas/xpath/XPath/HTML) for all
112
- available matchers.
113
-
114
76
  ## License
115
77
 
116
- (The MIT License)
117
-
118
- Copyright © 2010 Jonas Nicklas
119
-
120
- Permission is hereby granted, free of charge, to any person obtaining a copy of
121
- this software and associated documentation files (the ‘Software’), to deal in
122
- the Software without restriction, including without limitation the rights to
123
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
124
- of the Software, and to permit persons to whom the Software is furnished to do
125
- so, subject to the following conditions:
126
-
127
- The above copyright notice and this permission notice shall be included in all
128
- copies or substantial portions of the Software.
129
-
130
- THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
131
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
132
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
133
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
134
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
135
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
136
- SOFTWARE.
78
+ See [LICENSE](LICENSE).
data/lib/xpath/dsl.rb CHANGED
@@ -1,112 +1,174 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module XPath
2
4
  module DSL
3
- module TopLevel
4
- def current
5
- Expression.new(:this_node)
6
- end
5
+ def current
6
+ Expression.new(:this_node)
7
+ end
7
8
 
8
- def name
9
- Expression.new(:node_name, current)
10
- end
9
+ def descendant(*expressions)
10
+ Expression.new(:descendant, current, expressions)
11
+ end
11
12
 
12
- def descendant(*expressions)
13
- Expression.new(:descendant, current, expressions)
14
- end
13
+ def child(*expressions)
14
+ Expression.new(:child, current, expressions)
15
+ end
15
16
 
16
- def child(*expressions)
17
- Expression.new(:child, current, expressions)
18
- end
17
+ def axis(name, *element_names)
18
+ Expression.new(:axis, current, name, element_names)
19
+ end
19
20
 
20
- def axis(name, tag_name=:*)
21
- Expression.new(:axis, current, name, tag_name)
22
- end
21
+ def anywhere(*expressions)
22
+ Expression.new(:anywhere, expressions)
23
+ end
23
24
 
24
- def next_sibling(*expressions)
25
- Expression.new(:next_sibling, current, expressions)
26
- end
25
+ def attr(expression)
26
+ Expression.new(:attribute, current, expression)
27
+ end
27
28
 
28
- def previous_sibling(*expressions)
29
- Expression.new(:previous_sibling, current, expressions)
30
- end
29
+ def text
30
+ Expression.new(:text, current)
31
+ end
31
32
 
32
- def anywhere(*expressions)
33
- Expression.new(:anywhere, expressions)
34
- end
33
+ def css(selector)
34
+ Expression.new(:css, current, Literal.new(selector))
35
+ end
35
36
 
36
- def attr(expression)
37
- Expression.new(:attribute, current, expression)
38
- end
37
+ def function(name, *arguments)
38
+ Expression.new(:function, name, *arguments)
39
+ end
39
40
 
40
- def contains(expression)
41
- Expression.new(:contains, current, expression)
42
- end
41
+ def method(name, *arguments)
42
+ Expression.new(:function, name, current, *arguments)
43
+ end
43
44
 
44
- def starts_with(expression)
45
- Expression.new(:starts_with, current, expression)
45
+ def where(expression)
46
+ if expression
47
+ Expression.new(:where, current, expression)
48
+ else
49
+ current
46
50
  end
51
+ end
52
+ alias_method :[], :where
47
53
 
48
- def text
49
- Expression.new(:text, current)
50
- end
54
+ def is(expression)
55
+ Expression.new(:is, current, expression)
56
+ end
51
57
 
52
- def string
53
- Expression.new(:string_function, current)
54
- end
58
+ def binary_operator(name, rhs)
59
+ Expression.new(:binary_operator, name, current, rhs)
60
+ end
55
61
 
56
- def css(selector)
57
- Expression.new(:css, current, Literal.new(selector))
58
- end
62
+ def union(*expressions)
63
+ Union.new(*[self, expressions].flatten)
59
64
  end
65
+ alias_method :+, :union
60
66
 
61
- module ExpressionLevel
62
- include XPath::DSL::TopLevel
67
+ def last
68
+ function(:last)
69
+ end
63
70
 
64
- def where(expression)
65
- Expression.new(:where, current, expression)
66
- end
67
- alias_method :[], :where
71
+ def position
72
+ function(:position)
73
+ end
68
74
 
69
- def one_of(*expressions)
70
- Expression.new(:one_of, current, expressions)
75
+ METHODS = [
76
+ # node set
77
+ :count, :id, :local_name, :namespace_uri,
78
+ # string
79
+ :string, :concat, :starts_with, :contains, :substring_before,
80
+ :substring_after, :substring, :string_length, :normalize_space,
81
+ :translate,
82
+ # boolean
83
+ :boolean, :not, :true, :false, :lang,
84
+ # number
85
+ :number, :sum, :floor, :ceiling, :round
86
+ ].freeze
87
+
88
+ METHODS.each do |key|
89
+ name = key.to_s.tr('_', '-').to_sym
90
+ define_method key do |*args|
91
+ method(name, *args)
71
92
  end
93
+ end
72
94
 
73
- def equals(expression)
74
- Expression.new(:equality, current, expression)
75
- end
76
- alias_method :==, :equals
95
+ def qname
96
+ method(:name)
97
+ end
77
98
 
78
- def is(expression)
79
- Expression.new(:is, current, expression)
80
- end
99
+ alias_method :inverse, :not
100
+ alias_method :~, :not
101
+ alias_method :!, :not
102
+ alias_method :normalize, :normalize_space
103
+ alias_method :n, :normalize_space
104
+
105
+ OPERATORS = [
106
+ %i[equals = ==],
107
+ %i[or or |],
108
+ %i[and and &],
109
+ %i[not_equals != !=],
110
+ %i[lte <= <=],
111
+ %i[lt < <],
112
+ %i[gte >= >=],
113
+ %i[gt > >],
114
+ %i[plus +],
115
+ %i[minus -],
116
+ %i[multiply * *],
117
+ %i[divide div /],
118
+ %i[mod mod %]
119
+ ].freeze
120
+
121
+ OPERATORS.each do |(name, operator, alias_name)|
122
+ define_method name do |rhs|
123
+ binary_operator(operator, rhs)
124
+ end
125
+ alias_method alias_name, name if alias_name
126
+ end
81
127
 
82
- def or(expression)
83
- Expression.new(:or, current, expression)
84
- end
85
- alias_method :|, :or
128
+ AXES = %i[
129
+ ancestor ancestor_or_self attribute descendant_or_self
130
+ following following_sibling namespace parent preceding
131
+ preceding_sibling self
132
+ ].freeze
86
133
 
87
- def and(expression)
88
- Expression.new(:and, current, expression)
134
+ AXES.each do |key|
135
+ name = key.to_s.tr('_', '-').to_sym
136
+ define_method key do |*element_names|
137
+ axis(name, *element_names)
89
138
  end
90
- alias_method :&, :and
139
+ end
91
140
 
92
- def union(*expressions)
93
- Union.new(*[self, expressions].flatten)
94
- end
95
- alias_method :+, :union
141
+ alias_method :self_axis, :self
96
142
 
97
- def inverse
98
- Expression.new(:inverse, current)
99
- end
100
- alias_method :~, :inverse
143
+ def ends_with(suffix)
144
+ function(:substring, current, function(:'string-length', current).minus(function(:'string-length', suffix)).plus(1)) == suffix
145
+ end
101
146
 
102
- def string_literal
103
- Expression.new(:string_literal, self)
104
- end
147
+ def contains_word(word)
148
+ function(:concat, ' ', current.normalize_space, ' ').contains(" #{word} ")
149
+ end
105
150
 
106
- def normalize
107
- Expression.new(:normalized_space, current)
108
- end
109
- alias_method :n, :normalize
151
+ UPPERCASE_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ'
152
+ LOWERCASE_LETTERS = 'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ'
153
+
154
+ def lowercase
155
+ method(:translate, UPPERCASE_LETTERS, LOWERCASE_LETTERS)
156
+ end
157
+
158
+ def uppercase
159
+ method(:translate, LOWERCASE_LETTERS, UPPERCASE_LETTERS)
160
+ end
161
+
162
+ def one_of(*expressions)
163
+ expressions.map { |e| current.equals(e) }.reduce(:or)
164
+ end
165
+
166
+ def next_sibling(*expressions)
167
+ axis(:"following-sibling")[1].axis(:self, *expressions)
168
+ end
169
+
170
+ def previous_sibling(*expressions)
171
+ axis(:"preceding-sibling")[1].axis(:self, *expressions)
110
172
  end
111
173
  end
112
174
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module XPath
2
4
  class Expression
3
5
  attr_accessor :expression, :arguments
4
- include XPath::DSL::ExpressionLevel
6
+ include XPath::DSL
5
7
 
6
8
  def initialize(expression, *arguments)
7
9
  @expression = expression
@@ -12,7 +14,7 @@ module XPath
12
14
  self
13
15
  end
14
16
 
15
- def to_xpath(type=nil)
17
+ def to_xpath(type = nil)
16
18
  Renderer.render(self, type)
17
19
  end
18
20
  alias_method :to_s, :to_xpath
data/lib/xpath/literal.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module XPath
2
4
  class Literal
3
5
  attr_reader :value
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module XPath
2
4
  class Renderer
3
5
  def self.render(node, type)
@@ -15,11 +17,11 @@ module XPath
15
17
 
16
18
  def convert_argument(argument)
17
19
  case argument
18
- when Expression, Union then render(argument)
19
- when Array then argument.map { |element| convert_argument(element) }
20
- when String then string_literal(argument)
21
- when Literal then argument.value
22
- else argument.to_s
20
+ when Expression, Union then render(argument)
21
+ when Array then argument.map { |element| convert_argument(element) }
22
+ when String then string_literal(argument)
23
+ when Literal then argument.value
24
+ else argument.to_s
23
25
  end
24
26
  end
25
27
 
@@ -27,7 +29,7 @@ module XPath
27
29
  if string.include?("'")
28
30
  string = string.split("'", -1).map do |substr|
29
31
  "'#{substr}'"
30
- end.join(%q{,"'",})
32
+ end.join(%q(,"'",))
31
33
  "concat(#{string})"
32
34
  else
33
35
  "'#{string}'"
@@ -38,32 +40,20 @@ module XPath
38
40
  '.'
39
41
  end
40
42
 
41
- def descendant(parent, element_names)
42
- if element_names.length == 1
43
- "#{parent}//#{element_names.first}"
44
- elsif element_names.length > 1
45
- "#{parent}//*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]"
46
- else
47
- "#{parent}//*"
48
- end
43
+ def descendant(current, element_names)
44
+ with_element_conditions("#{current}//", element_names)
49
45
  end
50
46
 
51
- def child(parent, element_names)
52
- if element_names.length == 1
53
- "#{parent}/#{element_names.first}"
54
- elsif element_names.length > 1
55
- "#{parent}/*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]"
56
- else
57
- "#{parent}/*"
58
- end
47
+ def child(current, element_names)
48
+ with_element_conditions("#{current}/", element_names)
59
49
  end
60
50
 
61
- def axis(parent, name, tag_name)
62
- "#{parent}/#{name}::#{tag_name}"
51
+ def axis(current, name, element_names)
52
+ with_element_conditions("#{current}/#{name}::", element_names)
63
53
  end
64
54
 
65
- def node_name(current)
66
- "name(#{current})"
55
+ def anywhere(element_names)
56
+ with_element_conditions('//', element_names)
67
57
  end
68
58
 
69
59
  def where(on, condition)
@@ -71,18 +61,22 @@ module XPath
71
61
  end
72
62
 
73
63
  def attribute(current, name)
74
- "#{current}/@#{name}"
64
+ if valid_xml_name?(name)
65
+ "#{current}/@#{name}"
66
+ else
67
+ "#{current}/attribute::*[local-name(.) = #{string_literal(name)}]"
68
+ end
75
69
  end
76
70
 
77
- def equality(one, two)
78
- "#{one} = #{two}"
71
+ def binary_operator(name, left, right)
72
+ "(#{left} #{name} #{right})"
79
73
  end
80
74
 
81
75
  def is(one, two)
82
76
  if @type == :exact
83
- equality(one, two)
77
+ binary_operator('=', one, two)
84
78
  else
85
- contains(one, two)
79
+ function(:contains, one, two)
86
80
  end
87
81
  end
88
82
 
@@ -94,10 +88,6 @@ module XPath
94
88
  "#{current}/text()"
95
89
  end
96
90
 
97
- def normalized_space(current)
98
- "normalize-space(#{current})"
99
- end
100
-
101
91
  def literal(node)
102
92
  node
103
93
  end
@@ -113,62 +103,24 @@ module XPath
113
103
  expressions.join(' | ')
114
104
  end
115
105
 
116
- def anywhere(element_names)
117
- if element_names.length == 1
118
- "//#{element_names.first}"
119
- elsif element_names.length > 1
120
- "//*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]"
121
- else
122
- "//*"
123
- end
124
- end
125
-
126
- def contains(current, value)
127
- "contains(#{current}, #{value})"
128
- end
129
-
130
- def starts_with(current, value)
131
- "starts-with(#{current}, #{value})"
132
- end
133
-
134
- def and(one, two)
135
- "(#{one} and #{two})"
136
- end
137
-
138
- def or(one, two)
139
- "(#{one} or #{two})"
106
+ def function(name, *arguments)
107
+ "#{name}(#{arguments.join(', ')})"
140
108
  end
141
109
 
142
- def one_of(current, values)
143
- values.map { |value| "#{current} = #{value}" }.join(' or ')
144
- end
110
+ private
145
111
 
146
- def next_sibling(current, element_names)
112
+ def with_element_conditions(expression, element_names)
147
113
  if element_names.length == 1
148
- "#{current}/following-sibling::*[1]/self::#{element_names.first}"
114
+ "#{expression}#{element_names.first}"
149
115
  elsif element_names.length > 1
150
- "#{current}/following-sibling::*[1]/self::*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]"
116
+ "#{expression}*[#{element_names.map { |e| "self::#{e}" }.join(' | ')}]"
151
117
  else
152
- "#{current}/following-sibling::*[1]/self::*"
118
+ "#{expression}*"
153
119
  end
154
120
  end
155
121
 
156
- def previous_sibling(current, element_names)
157
- if element_names.length == 1
158
- "#{current}/preceding-sibling::*[1]/self::#{element_names.first}"
159
- elsif element_names.length > 1
160
- "#{current}/preceding-sibling::*[1]/self::*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]"
161
- else
162
- "#{current}/preceding-sibling::*[1]/self::*"
163
- end
164
- end
165
-
166
- def inverse(current)
167
- "not(#{current})"
168
- end
169
-
170
- def string_function(current)
171
- "string(#{current})"
122
+ def valid_xml_name?(name)
123
+ name =~ /^[a-zA-Z_:][a-zA-Z0-9_:\.\-]*$/
172
124
  end
173
125
  end
174
126
  end
data/lib/xpath/union.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module XPath
2
4
  class Union
3
5
  include Enumerable
@@ -17,11 +19,11 @@ module XPath
17
19
  arguments.each(&block)
18
20
  end
19
21
 
20
- def method_missing(*args)
22
+ def method_missing(*args) # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing
21
23
  XPath::Union.new(*arguments.map { |e| e.send(*args) })
22
24
  end
23
25
 
24
- def to_xpath(type=nil)
26
+ def to_xpath(type = nil)
25
27
  Renderer.render(self, type)
26
28
  end
27
29
  alias_method :to_s, :to_xpath
data/lib/xpath/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module XPath
2
- VERSION = '2.0.0'
4
+ VERSION = '3.2.0'
3
5
  end
data/lib/xpath.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'nokogiri'
2
4
 
3
5
  require 'xpath/dsl'
@@ -5,12 +7,10 @@ require 'xpath/expression'
5
7
  require 'xpath/literal'
6
8
  require 'xpath/union'
7
9
  require 'xpath/renderer'
8
- require 'xpath/html'
9
10
 
10
11
  module XPath
11
-
12
- extend XPath::DSL::TopLevel
13
- include XPath::DSL::TopLevel
12
+ extend XPath::DSL
13
+ include XPath::DSL
14
14
 
15
15
  def self.generate
16
16
  yield(self)
@@ -17,7 +17,7 @@
17
17
 
18
18
  <div id="woo" title="wooDiv" data="id">
19
19
  <ul><li>A list</li></ul>
20
- <p title="gorilla">Bax</p>
20
+ <p class="cat fish dog" title="gorilla">Bax</p>
21
21
  <p>Bax</p>
22
22
  <p id="wooDiv">Blah</p>
23
23
  </div>
@@ -26,7 +26,7 @@
26
26
 
27
27
  <div id="preference">
28
28
  <p id="is-fuzzy">allamas</p>
29
- <p id="is-exact">llama</p>
29
+ <p class="fish" id="is-exact">llama</p>
30
30
  </div>
31
31
 
32
32
  <p id="whitespace">
@@ -37,9 +37,17 @@
37
37
  </p>
38
38
 
39
39
  <div id="moar">
40
- <p id="impchay">chimp</p>
40
+ <p class="catfish" id="impchay">chimp</p>
41
41
  <div id="elephantay">elephant</div>
42
42
  <p id="amingoflay">flamingo</p>
43
43
  </div>
44
+
45
+ <span id="substring">Hello there</span>
46
+
47
+ <span id="string-length">Hello there</span>
48
+
49
+ <div id="oof" title="viDoof" data="id">
50
+ <p id="viDoof">Blah</p>
51
+ </div>
44
52
  </body>
45
53
  </html>
data/spec/spec_helper.rb CHANGED
@@ -1 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'xpath'
4
+ require 'pry'
5
+
6
+ RSpec.configure do |config|
7
+ config.expect_with(:rspec) { |c| c.syntax = :should }
8
+ end