xpath 2.0.0 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
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