wgibbs-xpath 0.1.4

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.
@@ -0,0 +1,56 @@
1
+ = XPath
2
+
3
+ XPath is a Ruby DSL around a subset of XPath 1.0. It's primary purpose is to
4
+ facilitate writing complex XPath queries from Ruby code.
5
+
6
+ == Generating expressions
7
+
8
+ To create quick, one of expressions, XPath.generate can be used:
9
+
10
+ XPath.generate { |x| x.descendant(:ul)[x.attr(:id) == 'foo'] }
11
+
12
+ However for more complex expressions, it is probably ore convenient to include
13
+ the XPath module into your own class or module:
14
+
15
+ module MyXPaths
16
+ include XPath
17
+
18
+ def foo_ul
19
+ descendant(:ul)[attr(:id) == 'foo']
20
+ end
21
+
22
+ def password_field(id)
23
+ descendant(:input)[attr(:type) == 'password'][attr(:id) == id]
24
+ end
25
+ end
26
+
27
+ Both ways return an XPath::Expression instance, which can be further modified.
28
+ To convert the expression to a string, just call #to_s on it.
29
+
30
+ == HTML
31
+
32
+ XPath comes with a set of premade XPaths for use with HTML documents.
33
+
34
+ == License
35
+
36
+ (The MIT License)
37
+
38
+ Copyright © 2010 Jonas Nicklas
39
+
40
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
41
+ this software and associated documentation files (the ‘Software’), to deal in
42
+ the Software without restriction, including without limitation the rights to
43
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
44
+ of the Software, and to permit persons to whom the Software is furnished to do
45
+ so, subject to the following conditions:
46
+
47
+ The above copyright notice and this permission notice shall be included in all
48
+ copies or substantial portions of the Software.
49
+
50
+ THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
51
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
52
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
53
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
54
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
55
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
56
+ SOFTWARE.
@@ -0,0 +1,68 @@
1
+ require 'nokogiri'
2
+
3
+ module XPath
4
+ autoload :Expression, 'xpath/expression'
5
+ autoload :Union, 'xpath/union'
6
+ autoload :HTML, 'xpath/html'
7
+
8
+ extend self
9
+
10
+ def self.generate
11
+ yield(Expression::Self.new)
12
+ end
13
+
14
+ def current
15
+ Expression::Self.new
16
+ end
17
+
18
+ def name
19
+ Expression::Name.new(current)
20
+ end
21
+
22
+ def descendant(*expressions)
23
+ Expression::Descendant.new(current, expressions)
24
+ end
25
+
26
+ def child(*expressions)
27
+ Expression::Child.new(current, expressions)
28
+ end
29
+
30
+ def anywhere(expression)
31
+ Expression::Anywhere.new(expression)
32
+ end
33
+
34
+ def attr(expression)
35
+ Expression::Attribute.new(current, expression)
36
+ end
37
+
38
+ def contains(expression)
39
+ Expression::Contains.new(current, expression)
40
+ end
41
+
42
+ def text
43
+ Expression::Text.new(current)
44
+ end
45
+
46
+ def var(name)
47
+ Expression::Variable.new(name)
48
+ end
49
+
50
+ def string
51
+ Expression::StringFunction.new(current)
52
+ end
53
+
54
+ def tag(name)
55
+ Expression::Tag.new(name)
56
+ end
57
+
58
+ def css(selector)
59
+ paths = Nokogiri::CSS.xpath_for(selector).map do |selector|
60
+ Expression::CSS.new(current, Expression::Literal.new(selector))
61
+ end
62
+ Union.new(*paths)
63
+ end
64
+
65
+ def varstring(name)
66
+ var(name).string_literal
67
+ end
68
+ end
@@ -0,0 +1,310 @@
1
+ module XPath
2
+ class Expression
3
+ include XPath
4
+
5
+ class Self < Expression
6
+ def to_xpath(predicate=nil)
7
+ '.'
8
+ end
9
+ end
10
+
11
+ class Unary < Expression
12
+ def initialize(expression)
13
+ @expression = wrap_xpath(expression)
14
+ end
15
+ end
16
+
17
+ class Binary < Expression
18
+ def initialize(left, right)
19
+ @left = wrap_xpath(left)
20
+ @right = wrap_xpath(right)
21
+ end
22
+ end
23
+
24
+ class Multiple < Expression
25
+ def initialize(left, expressions)
26
+ @left = wrap_xpath(left)
27
+ @expressions = expressions.map { |e| wrap_xpath(e) }
28
+ end
29
+ end
30
+
31
+ class Literal < Expression
32
+ def initialize(expression)
33
+ @expression = expression
34
+ end
35
+
36
+ def to_xpath(predicate=nil)
37
+ @expression.to_s
38
+ end
39
+ end
40
+
41
+ class Child < Multiple
42
+ def to_xpath(predicate=nil)
43
+ if @expressions.length == 1
44
+ "#{@left.to_xpath(predicate)}/#{@expressions.first.to_xpath(predicate)}"
45
+ elsif @expressions.length > 1
46
+ "#{@left.to_xpath(predicate)}/*[#{@expressions.map { |e| "self::#{e.to_xpath(predicate)}" }.join(" | ")}]"
47
+ else
48
+ "#{@left.to_xpath(predicate)}/*"
49
+ end
50
+ end
51
+ end
52
+
53
+ class Descendant < Multiple
54
+ def to_xpath(predicate=nil)
55
+ if @expressions.length == 1
56
+ "#{@left.to_xpath(predicate)}//#{@expressions.first.to_xpath(predicate)}"
57
+ elsif @expressions.length > 1
58
+ "#{@left.to_xpath(predicate)}//*[#{@expressions.map { |e| "self::#{e.to_xpath(predicate)}" }.join(" | ")}]"
59
+ else
60
+ "#{@left.to_xpath(predicate)}//*"
61
+ end
62
+ end
63
+ end
64
+
65
+ class NextSibling < Multiple
66
+ def to_xpath(predicate=nil)
67
+ if @expressions.length == 1
68
+ "#{@left.to_xpath(predicate)}/following-sibling::*[1]/self::#{@expressions.first.to_xpath(predicate)}"
69
+ elsif @expressions.length > 1
70
+ "#{@left.to_xpath(predicate)}/following-sibling::*[1]/self::*[#{@expressions.map { |e| "self::#{e.to_xpath(predicate)}" }.join(" | ")}]"
71
+ else
72
+ "#{@left.to_xpath(predicate)}/following-sibling::*[1]/self::*"
73
+ end
74
+ end
75
+ end
76
+
77
+ class Tag < Unary
78
+ def to_xpath(predicate=nil)
79
+ "self::#{@expression.to_xpath(predicate)}"
80
+ end
81
+ end
82
+
83
+ class Anywhere < Unary
84
+ def to_xpath(predicate=nil)
85
+ "//#{@expression.to_xpath(predicate)}"
86
+ end
87
+ end
88
+
89
+ class Name < Unary
90
+ def to_xpath(predicate=nil)
91
+ "name(#{@expression.to_xpath(predicate)})"
92
+ end
93
+ end
94
+
95
+ class Where < Binary
96
+ def to_xpath(predicate=nil)
97
+ "#{@left.to_xpath(predicate)}[#{@right.to_xpath(predicate)}]"
98
+ end
99
+ end
100
+
101
+ class Attribute < Binary
102
+ def to_xpath(predicate=nil)
103
+ if @right.is_a?(Literal)
104
+ "#{@left.to_xpath(predicate)}/@#{@right.to_xpath(predicate)}"
105
+ else
106
+ "#{@left.to_xpath(predicate)}/attribute::node()[name(.) = #{@right.to_xpath(predicate)}]"
107
+ end
108
+ end
109
+ end
110
+
111
+ class Equality < Binary
112
+ def to_xpath(predicate=nil)
113
+ "#{@left.to_xpath(predicate)} = #{@right.to_xpath(predicate)}"
114
+ end
115
+ end
116
+
117
+ class StringFunction < Unary
118
+ def to_xpath(predicate=nil)
119
+ "string(#{@expression.to_xpath(predicate)})"
120
+ end
121
+ end
122
+
123
+ class StringLiteral < Expression
124
+ def initialize(expression)
125
+ @expression = expression
126
+ end
127
+
128
+ def to_xpath(predicate=nil)
129
+ string = @expression
130
+ string = @expression.to_xpath(predicate) unless @expression.is_a?(String)
131
+ if string.include?("'")
132
+ string = string.split("'", -1).map do |substr|
133
+ "'#{substr}'"
134
+ end.join(%q{,"'",})
135
+ "concat(#{string})"
136
+ else
137
+ "'#{string}'"
138
+ end
139
+ end
140
+ end
141
+
142
+ class NormalizedSpace < Unary
143
+ def to_xpath(predicate=nil)
144
+ "normalize-space(#{@expression.to_xpath(predicate)})"
145
+ end
146
+ end
147
+
148
+ class And < Binary
149
+ def to_xpath(predicate=nil)
150
+ "(#{@left.to_xpath(predicate)} and #{@right.to_xpath(predicate)})"
151
+ end
152
+ end
153
+
154
+ class Or < Binary
155
+ def to_xpath(predicate=nil)
156
+ "(#{@left.to_xpath(predicate)} or #{@right.to_xpath(predicate)})"
157
+ end
158
+ end
159
+
160
+ class OneOf < Expression
161
+ def initialize(left, right)
162
+ @left = wrap_xpath(left)
163
+ @right = right.map { |r| wrap_xpath(r) }
164
+ end
165
+
166
+ def to_xpath(predicate=nil)
167
+ @right.map { |r| "#{@left.to_xpath(predicate)} = #{r.to_xpath(predicate)}" }.join(' or ')
168
+ end
169
+ end
170
+
171
+ class Contains < Binary
172
+ def to_xpath(predicate=nil)
173
+ "contains(#{@left.to_xpath(predicate)}, #{@right.to_xpath(predicate)})"
174
+ end
175
+ end
176
+
177
+ class Is < Binary
178
+ def to_xpath(predicate=nil)
179
+ if predicate == :exact
180
+ Equality.new(@left, @right).to_xpath(predicate)
181
+ else
182
+ Contains.new(@left, @right).to_xpath(predicate)
183
+ end
184
+ end
185
+ end
186
+
187
+ class Text < Unary
188
+ def to_xpath(predicate=nil)
189
+ "#{@expression.to_xpath(predicate)}/text()"
190
+ end
191
+ end
192
+
193
+ class Variable < Expression
194
+ def initialize(name)
195
+ @name = name
196
+ end
197
+
198
+ def to_xpath(predicate=nil)
199
+ "%{#{@name}}"
200
+ end
201
+ end
202
+
203
+ class Inverse < Unary
204
+ def to_xpath(predicate=nil)
205
+ "not(#{@expression.to_xpath(predicate)})"
206
+ end
207
+ end
208
+
209
+ class Applied < Expression
210
+ def initialize(expression, variables={})
211
+ @variables = variables
212
+ @expression = expression
213
+ end
214
+
215
+ def to_xpath(predicate=nil)
216
+ @expression.to_xpath(predicate) % @variables
217
+ rescue ArgumentError # for ruby < 1.9 compat
218
+ @expression.to_xpath(predicate).gsub(/%\{(\w+)\}/) do |_|
219
+ @variables[$1.to_sym] or raise(ArgumentError, "expected variable #{$1} to be set")
220
+ end
221
+ end
222
+ end
223
+
224
+ class CSS < Binary
225
+ def to_xpath(predicate=nil)
226
+ "#{@left.to_xpath}#{@right.to_xpath}"
227
+ end
228
+ end
229
+
230
+ def current
231
+ self
232
+ end
233
+
234
+ def next_sibling(*expressions)
235
+ Expression::NextSibling.new(current, expressions)
236
+ end
237
+
238
+ def where(expression)
239
+ Expression::Where.new(current, expression)
240
+ end
241
+ alias_method :[], :where
242
+
243
+ def one_of(*expressions)
244
+ Expression::OneOf.new(current, expressions)
245
+ end
246
+
247
+ def equals(expression)
248
+ Expression::Equality.new(current, expression)
249
+ end
250
+ alias_method :==, :equals
251
+
252
+ def is(expression)
253
+ Expression::Is.new(current, expression)
254
+ end
255
+
256
+ def or(expression)
257
+ Expression::Or.new(current, expression)
258
+ end
259
+ alias_method :|, :or
260
+
261
+ def and(expression)
262
+ Expression::And.new(current, expression)
263
+ end
264
+ alias_method :&, :and
265
+
266
+ def union(*expressions)
267
+ Union.new(*[self, expressions].flatten)
268
+ end
269
+ alias_method :+, :union
270
+
271
+ def inverse
272
+ Expression::Inverse.new(current)
273
+ end
274
+ alias_method :~, :inverse
275
+
276
+ def string_literal
277
+ Expression::StringLiteral.new(self)
278
+ end
279
+
280
+ def to_xpath(predicate=nil)
281
+ raise NotImplementedError, "please implement in subclass"
282
+ end
283
+
284
+ def to_s
285
+ to_xpaths.join(' | ')
286
+ end
287
+
288
+ def to_xpaths
289
+ [to_xpath(:exact), to_xpath(:fuzzy)].uniq
290
+ end
291
+
292
+ def apply(variables={})
293
+ Expression::Applied.new(current, variables)
294
+ end
295
+
296
+ def normalize
297
+ Expression::NormalizedSpace.new(current)
298
+ end
299
+ alias_method :n, :normalize
300
+
301
+ def wrap_xpath(expression)
302
+ case expression
303
+ when ::String then Expression::StringLiteral.new(expression)
304
+ when ::Symbol then Expression::Literal.new(expression)
305
+ else expression
306
+ end
307
+ end
308
+ end
309
+ end
310
+
@@ -0,0 +1,114 @@
1
+ module XPath
2
+ module HTML
3
+ include XPath
4
+ extend self
5
+
6
+ def link(locator, options={})
7
+ href = options[:href]
8
+ link = descendant(:a)[href ? attr(:href).equals(href) : attr(:href)]
9
+ link[attr(:id).equals(locator) | string.n.is(locator) | attr(:title).is(locator) | descendant(:img)[attr(:alt).is(locator)]]
10
+ end
11
+
12
+ def content(locator)
13
+ child(:"descendant-or-self::*")[current.n.contains(locator)]
14
+ end
15
+
16
+ def button(locator)
17
+ button = descendant(:input)[attr(:type).one_of('submit', 'image', 'button')][attr(:id).equals(locator) | attr(:value).is(locator)]
18
+ button += descendant(:button)[attr(:id).equals(locator) | attr(:value).is(locator) | string.n.is(locator)]
19
+ button += descendant(:input)[attr(:type).equals('image')][attr(:alt).is(locator)]
20
+ end
21
+
22
+ def link_or_button(locator)
23
+ link(locator) + button(locator)
24
+ end
25
+
26
+ def fieldset(locator)
27
+ descendant(:fieldset)[attr(:id).equals(locator) | descendant(:legend)[string.n.is(locator)]]
28
+ end
29
+
30
+ def field(locator, options={})
31
+ xpath = descendant(:input, :textarea, :select)[~attr(:type).one_of('submit', 'image', 'hidden')]
32
+ xpath = locate_field(xpath, locator)
33
+ xpath = xpath[attr(:checked)] if options[:checked]
34
+ xpath = xpath[~attr(:checked)] if options[:unchecked]
35
+ xpath = xpath[field_value(options[:with])] if options.has_key?(:with)
36
+ xpath
37
+ end
38
+
39
+ def fillable_field(locator, options={})
40
+ xpath = descendant(:input, :textarea)[~attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')]
41
+ xpath = locate_field(xpath, locator)
42
+ xpath = xpath[field_value(options[:with])] if options.has_key?(:with)
43
+ xpath
44
+ end
45
+
46
+ def select(locator, options={})
47
+ xpath = locate_field(descendant(:select), locator)
48
+
49
+ options[:options].each do |option|
50
+ xpath = xpath[descendant(:option).equals(option)]
51
+ end if options[:options]
52
+
53
+ [options[:selected]].flatten.each do |option|
54
+ xpath = xpath[descendant(:option)[attr(:selected)].equals(option)]
55
+ end if options[:selected]
56
+
57
+ xpath
58
+ end
59
+
60
+ def checkbox(locator, options={})
61
+ xpath = locate_field(descendant(:input)[attr(:type).equals('checkbox')], locator)
62
+ end
63
+
64
+ def radio_button(locator, options={})
65
+ locate_field(descendant(:input)[attr(:type).equals('radio')], locator)
66
+ end
67
+
68
+ def file_field(locator, options={})
69
+ locate_field(descendant(:input)[attr(:type).equals('file')], locator)
70
+ end
71
+
72
+ def optgroup(name)
73
+ descendant(:optgroup)[attr(:label).is(name)]
74
+ end
75
+
76
+ def option(name)
77
+ descendant(:option)[string.n.is(name)]
78
+ end
79
+
80
+ def table(locator, options={})
81
+ xpath = descendant(:table)[attr(:id).equals(locator) | descendant(:caption).contains(locator)]
82
+ xpath = xpath[table_rows(options[:rows])] if options[:rows]
83
+ xpath
84
+ end
85
+
86
+ def table_rows(rows)
87
+ row_conditions = descendant(:tr)[table_row(rows.first)]
88
+ rows.drop(1).each do |row|
89
+ row_conditions = row_conditions.next_sibling(:tr)[table_row(row)]
90
+ end
91
+ row_conditions
92
+ end
93
+
94
+ def table_row(cells)
95
+ cell_conditions = child(:td, :th)[string.n.equals(cells.first)]
96
+ cells.drop(1).each do |cell|
97
+ cell_conditions = cell_conditions.next_sibling(:td, :th)[string.n.equals(cell)]
98
+ end
99
+ cell_conditions
100
+ end
101
+
102
+ protected
103
+
104
+ def locate_field(xpath, locator)
105
+ locate_field = xpath[attr(:id).equals(locator) | attr(:name).equals(locator) | attr(:id).equals(anywhere(:label)[string.n.is(locator)].attr(:for)) | attr(:placeholder).equals(locator)]
106
+ locate_field += descendant(:label)[string.n.is(locator)].descendant(xpath)
107
+ end
108
+
109
+ def field_value(value)
110
+ (string.n.is(value) & tag(:textarea)) | (attr(:value).equals(value) & ~tag(:textarea))
111
+ end
112
+
113
+ end
114
+ end