wgibbs-xpath 0.1.4

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