xpath 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +29 -0
- data/lib/xpath.rb +51 -0
- data/lib/xpath/expression.rb +298 -0
- data/lib/xpath/html.rb +124 -0
- data/lib/xpath/union.rb +27 -0
- data/lib/xpath/version.rb +3 -0
- data/spec/fixtures/form.html +245 -0
- data/spec/fixtures/simple.html +26 -0
- data/spec/fixtures/stuff.html +43 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/union_spec.rb +63 -0
- data/spec/xpath_spec.rb +301 -0
- metadata +127 -0
data/README.rdoc
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
= XPath
|
2
|
+
|
3
|
+
XPath is a Ruby DSL around a subset of XPath 1.0. It's primary purpose is to facilitate writing complex XPath queries from Ruby code.
|
4
|
+
|
5
|
+
== Generating expressions
|
6
|
+
|
7
|
+
To create quick, one of expressions, XPath.generate can be used:
|
8
|
+
|
9
|
+
XPath.generate { |x| x.descendant(:ul)[x.attr(:id) == 'foo'] }
|
10
|
+
|
11
|
+
However for more complex expressions, it is probably ore convenient to include the XPath module into your own class or module:
|
12
|
+
|
13
|
+
module MyXPaths
|
14
|
+
include XPath
|
15
|
+
|
16
|
+
def foo_ul
|
17
|
+
descendant(:ul)[attr(:id) == 'foo']
|
18
|
+
end
|
19
|
+
|
20
|
+
def password_field(id)
|
21
|
+
descendant(:input)[attr(:type) == 'password'][attr(:id) == id]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
Both ways return an XPath::Expression instance, which can be further modified. To convert the expression to a string, just call #to_s on it.
|
26
|
+
|
27
|
+
== HTML
|
28
|
+
|
29
|
+
XPath comes with a set of premade XPaths for use with HTML documents.
|
data/lib/xpath.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module XPath
|
2
|
+
autoload :Expression, 'xpath/expression'
|
3
|
+
autoload :Union, 'xpath/union'
|
4
|
+
autoload :HTML, 'xpath/html'
|
5
|
+
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def self.generate
|
9
|
+
yield(Expression::Self.new)
|
10
|
+
end
|
11
|
+
|
12
|
+
def current
|
13
|
+
Expression::Self.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def name
|
17
|
+
Expression::Name.new(current)
|
18
|
+
end
|
19
|
+
|
20
|
+
def descendant(*expressions)
|
21
|
+
Expression::Descendant.new(current, expressions)
|
22
|
+
end
|
23
|
+
|
24
|
+
def child(*expressions)
|
25
|
+
Expression::Child.new(current, expressions)
|
26
|
+
end
|
27
|
+
|
28
|
+
def anywhere(expression)
|
29
|
+
Expression::Anywhere.new(expression)
|
30
|
+
end
|
31
|
+
|
32
|
+
def attr(expression)
|
33
|
+
Expression::Attribute.new(current, expression)
|
34
|
+
end
|
35
|
+
|
36
|
+
def contains(expression)
|
37
|
+
Expression::Contains.new(current, expression)
|
38
|
+
end
|
39
|
+
|
40
|
+
def text
|
41
|
+
Expression::Text.new(current)
|
42
|
+
end
|
43
|
+
|
44
|
+
def var(name)
|
45
|
+
Expression::Variable.new(name)
|
46
|
+
end
|
47
|
+
|
48
|
+
def varstring(name)
|
49
|
+
var(name).string_literal
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,298 @@
|
|
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
|
+
if @expressions.empty?
|
29
|
+
raise ArgumentError, "must specify at least one expression"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Literal < Expression
|
35
|
+
def initialize(expression)
|
36
|
+
@expression = expression
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_xpath(predicate=nil)
|
40
|
+
@expression.to_s
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class Child < Multiple
|
45
|
+
def to_xpath(predicate=nil)
|
46
|
+
if @expressions.length == 1
|
47
|
+
"#{@left.to_xpath(predicate)}/#{@expressions.first.to_xpath(predicate)}"
|
48
|
+
else
|
49
|
+
"#{@left.to_xpath(predicate)}/*[#{@expressions.map { |e| "self::#{e.to_xpath(predicate)}" }.join(" | ")}]"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Descendant < Multiple
|
55
|
+
def to_xpath(predicate=nil)
|
56
|
+
if @expressions.length == 1
|
57
|
+
"#{@left.to_xpath(predicate)}//#{@expressions.first.to_xpath(predicate)}"
|
58
|
+
else
|
59
|
+
"#{@left.to_xpath(predicate)}//*[#{@expressions.map { |e| "self::#{e.to_xpath(predicate)}" }.join(" | ")}]"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class NextSibling < Multiple
|
65
|
+
def to_xpath(predicate=nil)
|
66
|
+
if @expressions.length == 1
|
67
|
+
"#{@left.to_xpath(predicate)}/following-sibling::*[1]/self::#{@expressions.first.to_xpath(predicate)}"
|
68
|
+
else
|
69
|
+
"#{@left.to_xpath(predicate)}/following-sibling::*[1]/self::*[#{@expressions.map { |e| "self::#{e.to_xpath(predicate)}" }.join(" | ")}]"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Anywhere < Unary
|
75
|
+
def to_xpath(predicate=nil)
|
76
|
+
"//#{@expression.to_xpath(predicate)}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Name < Unary
|
81
|
+
def to_xpath(predicate=nil)
|
82
|
+
"name(#{@expression.to_xpath(predicate)})"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class Where < Binary
|
87
|
+
def to_xpath(predicate=nil)
|
88
|
+
"#{@left.to_xpath(predicate)}[#{@right.to_xpath(predicate)}]"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class Attribute < Binary
|
93
|
+
def to_xpath(predicate=nil)
|
94
|
+
if @right.is_a?(Literal)
|
95
|
+
"#{@left.to_xpath(predicate)}/@#{@right.to_xpath(predicate)}"
|
96
|
+
else
|
97
|
+
"#{@left.to_xpath(predicate)}/attribute::node()[name(.) = #{@right.to_xpath(predicate)}]"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class Equality < Binary
|
103
|
+
def to_xpath(predicate=nil)
|
104
|
+
"#{@left.to_xpath(predicate)} = #{@right.to_xpath(predicate)}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class StringFunction < Unary
|
109
|
+
def to_xpath(predicate=nil)
|
110
|
+
"string(#{@expression.to_xpath(predicate)})"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class StringLiteral < Expression
|
115
|
+
def initialize(expression)
|
116
|
+
@expression = expression
|
117
|
+
end
|
118
|
+
|
119
|
+
def to_xpath(predicate=nil)
|
120
|
+
@expression = @expression.to_xpath(predicate) unless @expression.is_a?(String)
|
121
|
+
if @expression.include?("'")
|
122
|
+
@expression = @expression.split("'", -1).map do |substr|
|
123
|
+
"'#{substr}'"
|
124
|
+
end.join(%q{,"'",})
|
125
|
+
"concat(#{@expression})"
|
126
|
+
else
|
127
|
+
"'#{@expression}'"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class NormalizedSpace < Unary
|
133
|
+
def to_xpath(predicate=nil)
|
134
|
+
"normalize-space(#{@expression.to_xpath(predicate)})"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class And < Binary
|
139
|
+
def to_xpath(predicate=nil)
|
140
|
+
"(#{@left.to_xpath(predicate)} and #{@right.to_xpath(predicate)})"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class Or < Binary
|
145
|
+
def to_xpath(predicate=nil)
|
146
|
+
"(#{@left.to_xpath(predicate)} or #{@right.to_xpath(predicate)})"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class OneOf < Expression
|
151
|
+
def initialize(left, right)
|
152
|
+
@left = wrap_xpath(left)
|
153
|
+
@right = right.map { |r| wrap_xpath(r) }
|
154
|
+
end
|
155
|
+
|
156
|
+
def to_xpath(predicate=nil)
|
157
|
+
@right.map { |r| "#{@left.to_xpath(predicate)} = #{r.to_xpath(predicate)}" }.join(' or ')
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
class Contains < Binary
|
162
|
+
def to_xpath(predicate=nil)
|
163
|
+
"contains(#{@left.to_xpath(predicate)}, #{@right.to_xpath(predicate)})"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
class Is < Binary
|
168
|
+
def to_xpath(predicate=nil)
|
169
|
+
if predicate == :exact
|
170
|
+
Equality.new(@left, @right).to_xpath(predicate)
|
171
|
+
else
|
172
|
+
Contains.new(@left, @right).to_xpath(predicate)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
class Text < Unary
|
178
|
+
def to_xpath(predicate=nil)
|
179
|
+
"#{@expression.to_xpath(predicate)}/text()"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
class Variable < Expression
|
184
|
+
def initialize(name)
|
185
|
+
@name = name
|
186
|
+
end
|
187
|
+
|
188
|
+
def to_xpath(predicate=nil)
|
189
|
+
"%{#{@name}}"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
class Inverse < Unary
|
194
|
+
def to_xpath(predicate=nil)
|
195
|
+
"not(#{@expression.to_xpath(predicate)})"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
class Applied < Expression
|
200
|
+
def initialize(expression, variables={})
|
201
|
+
@variables = variables
|
202
|
+
@expression = expression
|
203
|
+
end
|
204
|
+
|
205
|
+
def to_xpath(predicate=nil)
|
206
|
+
@expression.to_xpath(predicate) % @variables
|
207
|
+
rescue ArgumentError # for ruby < 1.9 compat
|
208
|
+
@expression.to_xpath(predicate).gsub(/%\{(\w+)\}/) do |_|
|
209
|
+
@variables[$1.to_sym] or raise(ArgumentError, "expected variable #{$1} to be set")
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def current
|
215
|
+
self
|
216
|
+
end
|
217
|
+
|
218
|
+
def next_sibling(*expressions)
|
219
|
+
Expression::NextSibling.new(current, expressions)
|
220
|
+
end
|
221
|
+
|
222
|
+
def where(expression)
|
223
|
+
Expression::Where.new(current, expression)
|
224
|
+
end
|
225
|
+
alias_method :[], :where
|
226
|
+
|
227
|
+
def one_of(*expressions)
|
228
|
+
Expression::OneOf.new(current, expressions)
|
229
|
+
end
|
230
|
+
|
231
|
+
def equals(expression)
|
232
|
+
Expression::Equality.new(current, expression)
|
233
|
+
end
|
234
|
+
alias_method :==, :equals
|
235
|
+
|
236
|
+
def is(expression)
|
237
|
+
Expression::Is.new(current, expression)
|
238
|
+
end
|
239
|
+
|
240
|
+
def string
|
241
|
+
Expression::StringFunction.new(current)
|
242
|
+
end
|
243
|
+
|
244
|
+
def or(expression)
|
245
|
+
Expression::Or.new(current, expression)
|
246
|
+
end
|
247
|
+
alias_method :|, :or
|
248
|
+
|
249
|
+
def and(expression)
|
250
|
+
Expression::And.new(current, expression)
|
251
|
+
end
|
252
|
+
alias_method :&, :and
|
253
|
+
|
254
|
+
def union(*expressions)
|
255
|
+
Union.new(*[self, expressions].flatten)
|
256
|
+
end
|
257
|
+
alias_method :+, :union
|
258
|
+
|
259
|
+
def inverse
|
260
|
+
Expression::Inverse.new(current)
|
261
|
+
end
|
262
|
+
alias_method :~, :inverse
|
263
|
+
|
264
|
+
def string_literal
|
265
|
+
Expression::StringLiteral.new(self)
|
266
|
+
end
|
267
|
+
|
268
|
+
def to_xpath(predicate=nil)
|
269
|
+
raise NotImplementedError, "please implement in subclass"
|
270
|
+
end
|
271
|
+
|
272
|
+
def to_s
|
273
|
+
to_xpaths.join(' | ')
|
274
|
+
end
|
275
|
+
|
276
|
+
def to_xpaths
|
277
|
+
[to_xpath(:exact), to_xpath(:fuzzy)].uniq
|
278
|
+
end
|
279
|
+
|
280
|
+
def apply(variables={})
|
281
|
+
Expression::Applied.new(current, variables)
|
282
|
+
end
|
283
|
+
|
284
|
+
def normalize
|
285
|
+
Expression::NormalizedSpace.new(current)
|
286
|
+
end
|
287
|
+
alias_method :n, :normalize
|
288
|
+
|
289
|
+
def wrap_xpath(expression)
|
290
|
+
case expression
|
291
|
+
when ::String then Expression::StringLiteral.new(expression)
|
292
|
+
when ::Symbol then Expression::Literal.new(expression)
|
293
|
+
else expression
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
data/lib/xpath/html.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
module XPath
|
2
|
+
module HTML
|
3
|
+
include XPath
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def from_css(css)
|
7
|
+
XPath::Union.new(*Nokogiri::CSS.xpath_for(css).map { |selector| ::XPath::Expression::Literal.new(:".#{selector}") }.flatten)
|
8
|
+
end
|
9
|
+
|
10
|
+
def link(locator)
|
11
|
+
link = descendant(:a)[attr(:href)]
|
12
|
+
link[attr(:id).equals(locator) | text.is(locator) | attr(:title).is(locator) | descendant(:img)[attr(:alt).is(locator)]]
|
13
|
+
end
|
14
|
+
|
15
|
+
def content(locator)
|
16
|
+
child(:"descendant-or-self::*")[current.n.contains(locator)]
|
17
|
+
end
|
18
|
+
|
19
|
+
def button(locator)
|
20
|
+
button = descendant(:input)[attr(:type).one_of('submit', 'image', 'button')][attr(:id).equals(locator) | attr(:value).is(locator)]
|
21
|
+
button += descendant(:button)[attr(:id).equals(locator) | attr(:value).is(locator) | text.is(locator)]
|
22
|
+
button += descendant(:input)[attr(:type).equals('image')][attr(:alt).is(locator)]
|
23
|
+
end
|
24
|
+
|
25
|
+
def link_or_button(locator)
|
26
|
+
link(locator) + button(locator)
|
27
|
+
end
|
28
|
+
|
29
|
+
def fieldset(locator)
|
30
|
+
descendant(:fieldset)[attr(:id).equals(locator) | descendant(:legend)[text.is(locator)]]
|
31
|
+
end
|
32
|
+
|
33
|
+
def field(locator, options={})
|
34
|
+
if options[:with]
|
35
|
+
fillable_field(locator, options)
|
36
|
+
else
|
37
|
+
xpath = descendant(:input, :textarea, :select)[~attr(:type).one_of('submit', 'image', 'hidden')]
|
38
|
+
xpath = locate_field(xpath, locator)
|
39
|
+
xpath = xpath[attr(:checked)] if options[:checked]
|
40
|
+
xpath = xpath[~attr(:checked)] if options[:unchecked]
|
41
|
+
xpath
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def fillable_field(locator, options={})
|
46
|
+
xpath = descendant(:input, :textarea)[~attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')]
|
47
|
+
xpath = locate_field(xpath, locator)
|
48
|
+
xpath = xpath[field_value(options[:with])] if options.has_key?(:with)
|
49
|
+
xpath
|
50
|
+
end
|
51
|
+
|
52
|
+
def select(locator, options={})
|
53
|
+
xpath = locate_field(descendant(:select), locator)
|
54
|
+
|
55
|
+
options[:options].each do |option|
|
56
|
+
xpath = xpath[descendant(:option).text.equals(option)]
|
57
|
+
end if options[:options]
|
58
|
+
|
59
|
+
[options[:selected]].flatten.each do |option|
|
60
|
+
xpath = xpath[descendant(:option)[attr(:selected)].text.equals(option)]
|
61
|
+
end if options[:selected]
|
62
|
+
|
63
|
+
xpath
|
64
|
+
end
|
65
|
+
|
66
|
+
def checkbox(locator, options={})
|
67
|
+
xpath = locate_field(descendant(:input)[attr(:type).equals('checkbox')], locator)
|
68
|
+
end
|
69
|
+
|
70
|
+
def radio_button(locator, options={})
|
71
|
+
locate_field(descendant(:input)[attr(:type).equals('radio')], locator)
|
72
|
+
end
|
73
|
+
|
74
|
+
def file_field(locator, options={})
|
75
|
+
locate_field(descendant(:input)[attr(:type).equals('file')], locator)
|
76
|
+
end
|
77
|
+
|
78
|
+
def option(name)
|
79
|
+
descendant(:option)[text.n.is(name)]
|
80
|
+
end
|
81
|
+
|
82
|
+
def table(locator, options={})
|
83
|
+
xpath = descendant(:table)[attr(:id).equals(locator) | descendant(:caption).contains(locator)]
|
84
|
+
xpath = xpath[table_rows(options[:rows])] if options[:rows]
|
85
|
+
xpath
|
86
|
+
end
|
87
|
+
|
88
|
+
def table_rows(rows)
|
89
|
+
row_conditions = descendant(:tr)[table_row(rows.first)]
|
90
|
+
rows.drop(1).each do |row|
|
91
|
+
row_conditions = row_conditions.next_sibling(:tr)[table_row(row)]
|
92
|
+
end
|
93
|
+
row_conditions
|
94
|
+
end
|
95
|
+
|
96
|
+
def table_row(cells)
|
97
|
+
cell_conditions = child(:td, :th)[text.equals(cells.first)]
|
98
|
+
cells.drop(1).each do |cell|
|
99
|
+
cell_conditions = cell_conditions.next_sibling(:td, :th)[text.equals(cell)]
|
100
|
+
end
|
101
|
+
cell_conditions
|
102
|
+
end
|
103
|
+
|
104
|
+
def wrap(path)
|
105
|
+
if path.respond_to?(:to_xpaths)
|
106
|
+
path.to_xpaths
|
107
|
+
else
|
108
|
+
[path.to_s].flatten
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
def locate_field(xpath, locator)
|
115
|
+
locate_field = xpath[attr(:id).equals(locator) | attr(:name).equals(locator) | attr(:id).equals(anywhere(:label)[text.is(locator)].attr(:for))]
|
116
|
+
locate_field += descendant(:label)[text.is(locator)].descendant(xpath)
|
117
|
+
end
|
118
|
+
|
119
|
+
def field_value(value)
|
120
|
+
(text.is(value) & name.equals('textarea')) | (attr(:value).equals(value) & ~name.equals('textarea'))
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
data/lib/xpath/union.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module XPath
|
2
|
+
class Union
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_reader :expressions
|
6
|
+
|
7
|
+
def initialize(*expressions)
|
8
|
+
@expressions = expressions
|
9
|
+
end
|
10
|
+
|
11
|
+
def each(&block)
|
12
|
+
expressions.each(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_xpath(predicate=nil)
|
16
|
+
expressions.map { |e| e.to_xpath(predicate) }.join(' | ')
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_xpaths
|
20
|
+
[to_xpath(:exact), to_xpath(:fuzzy)].uniq
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_missing(*args)
|
24
|
+
XPath::Union.new(*expressions.map { |e| e.send(*args) })
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,245 @@
|
|
1
|
+
<h1>Form</h1>
|
2
|
+
|
3
|
+
<form action="/form" method="post">
|
4
|
+
|
5
|
+
<p>
|
6
|
+
<label for="form_title">Title</label>
|
7
|
+
<select name="form[title]" id="form_title">
|
8
|
+
<option>Mrs</option>
|
9
|
+
<option>Mr</option>
|
10
|
+
<option>Miss</option>
|
11
|
+
</select>
|
12
|
+
</p>
|
13
|
+
|
14
|
+
|
15
|
+
<p>
|
16
|
+
<label for="form_other_title">Other title</label>
|
17
|
+
<select name="form[other_title]" id="form_other_title">
|
18
|
+
<option>Mrs</option>
|
19
|
+
<option>Mr</option>
|
20
|
+
<option>Miss</option>
|
21
|
+
</select>
|
22
|
+
</p>
|
23
|
+
|
24
|
+
<p>
|
25
|
+
<label for="form_first_name">
|
26
|
+
First Name
|
27
|
+
<input type="text" name="form[first_name]" value="John" id="form_first_name"/>
|
28
|
+
</label>
|
29
|
+
</p>
|
30
|
+
|
31
|
+
<p>
|
32
|
+
<label for="form_last_name">Last Name</label>
|
33
|
+
<input type="text" name="form[last_name]" value="Smith" id="form_last_name"/>
|
34
|
+
</p>
|
35
|
+
|
36
|
+
<p>
|
37
|
+
<label for="form_name_explanation">Explanation of Name</label>
|
38
|
+
<textarea name="form[name_explanation]" id="form_name_explanation"></textarea>
|
39
|
+
</p>
|
40
|
+
|
41
|
+
<p>
|
42
|
+
<label for="form_name">Name</label>
|
43
|
+
<input type="text" name="form[name]" value="John Smith" id="form_name"/>
|
44
|
+
</p>
|
45
|
+
|
46
|
+
<p>
|
47
|
+
<label for="form_schmooo">Schmooo</label>
|
48
|
+
<input type="schmooo" name="form[schmooo]" value="This is Schmooo!" id="form_schmooo"/>
|
49
|
+
</p>
|
50
|
+
|
51
|
+
<p>
|
52
|
+
<label>Street<br/>
|
53
|
+
<input type="text" name="form[street]" value="Sesame street 66"/>
|
54
|
+
</label>
|
55
|
+
</p>
|
56
|
+
|
57
|
+
<p>
|
58
|
+
<label for="form_phone">Phone</label>
|
59
|
+
<input name="form[phone]" value="+1 555 7021" id="form_phone"/>
|
60
|
+
</p>
|
61
|
+
|
62
|
+
<p>
|
63
|
+
<label for="form_password">Password</label>
|
64
|
+
<input type="password" name="form[password]" value="seeekrit" id="form_password"/>
|
65
|
+
</p>
|
66
|
+
|
67
|
+
<p>
|
68
|
+
<label for="form_terms_of_use">Terms of Use</label>
|
69
|
+
<input type="hidden" name="form[terms_of_use]" value="0" id="form_terms_of_use_default">
|
70
|
+
<input type="checkbox" name="form[terms_of_use]" value="1" id="form_terms_of_use">
|
71
|
+
</p>
|
72
|
+
|
73
|
+
<p>
|
74
|
+
<label for="form_image">Image</label>
|
75
|
+
<input type="file" name="form[image]" id="form_image"/>
|
76
|
+
</p>
|
77
|
+
|
78
|
+
<p>
|
79
|
+
<input type="hidden" name="form[token]" value="12345" id="form_token"/>
|
80
|
+
</p>
|
81
|
+
|
82
|
+
<p>
|
83
|
+
<label for="form_locale">Locale</label>
|
84
|
+
<select name="form[locale]" id="form_locale">
|
85
|
+
<option value="sv">Swedish</option>
|
86
|
+
<option selected="selected" value="en">English</option>
|
87
|
+
<option value="fi">Finish</option>
|
88
|
+
<option value="no">Norwegian</option>
|
89
|
+
<option value="jo">John's made-up language</option>
|
90
|
+
<option value="jbo"> Lojban </option>
|
91
|
+
</select>
|
92
|
+
</p>
|
93
|
+
|
94
|
+
<p>
|
95
|
+
<label for="form_region">Region</label>
|
96
|
+
<select name="form[region]" id="form_region">
|
97
|
+
<option>Sweden</option>
|
98
|
+
<option selected="selected">Norway</option>
|
99
|
+
<option>Finland</option>
|
100
|
+
</select>
|
101
|
+
</p>
|
102
|
+
|
103
|
+
<p>
|
104
|
+
<label for="form_city">City</label>
|
105
|
+
<select name="form[city]" id="form_city">
|
106
|
+
<option>London</option>
|
107
|
+
<option>Stockholm</option>
|
108
|
+
<option>Paris</option>
|
109
|
+
</select>
|
110
|
+
</p>
|
111
|
+
|
112
|
+
<p>
|
113
|
+
<label for="form_tendency">Tendency</label>
|
114
|
+
<select name="form[tendency]" id="form_tendency"></select>
|
115
|
+
</p>
|
116
|
+
|
117
|
+
<p>
|
118
|
+
<label for="form_description">Description</label></br>
|
119
|
+
<textarea name="form[description]" id="form_description">Descriptive text goes here</textarea>
|
120
|
+
<p>
|
121
|
+
|
122
|
+
<p>
|
123
|
+
<input type="radio" name="form[gender]" value="male" id="gender_male"/>
|
124
|
+
<label for="gender_male">Male</label>
|
125
|
+
<input type="radio" name="form[gender]" value="female" id="gender_female" checked="checked"/>
|
126
|
+
<label for="gender_female">Female</label>
|
127
|
+
<input type="radio" name="form[gender]" value="both" id="gender_both"/>
|
128
|
+
<label for="gender_both">Both</label>
|
129
|
+
</p>
|
130
|
+
|
131
|
+
<p>
|
132
|
+
<input type="checkbox" value="dog" name="form[pets][]" id="form_pets_dog" checked="checked"/>
|
133
|
+
<label for="form_pets_dog">Dog</label>
|
134
|
+
<input type="checkbox" value="cat" name="form[pets][]" id="form_pets_cat"/>
|
135
|
+
<label for="form_pets_cat">Cat</label>
|
136
|
+
<input type="checkbox" value="hamster" name="form[pets][]" id="form_pets_hamster" checked="checked"/>
|
137
|
+
<label for="form_pets_hamster">Hamster</label>
|
138
|
+
</p>
|
139
|
+
|
140
|
+
<p>
|
141
|
+
<label for="form_languages">Languages</label>
|
142
|
+
<select name="form[languages][]" id="form_languages" multiple="multiple">
|
143
|
+
<option>Ruby</option>
|
144
|
+
<option>SQL</option>
|
145
|
+
<option>HTML</option>
|
146
|
+
<option>Javascript</option>
|
147
|
+
</select>
|
148
|
+
</p>
|
149
|
+
|
150
|
+
<p>
|
151
|
+
<label for="form_underwear">Underwear</label>
|
152
|
+
<select name="form[underwear][]" id="form_underwear" multiple="multiple">
|
153
|
+
<option selected="selected">Boxer Briefs</option>
|
154
|
+
<option>Boxers</option>
|
155
|
+
<option selected="selected">Briefs</option>
|
156
|
+
<option selected="selected">Commando</option>
|
157
|
+
<option selected="selected">Frenchman's Pantalons</option>
|
158
|
+
</select>
|
159
|
+
</p>
|
160
|
+
|
161
|
+
<div style="display:none;">
|
162
|
+
<label for="form_first_name_hidden">
|
163
|
+
Super Secret
|
164
|
+
<input type="text" name="form[super_secret]" value="test123" id="form_super_secret"/>
|
165
|
+
</label>
|
166
|
+
</div>
|
167
|
+
|
168
|
+
<p>
|
169
|
+
<input type="button" name="form[fresh]" id="fresh_btn" value="i am fresh"/>
|
170
|
+
<input type="submit" name="form[awesome]" id="awe123" value="awesome"/>
|
171
|
+
<input type="submit" name="form[crappy]" id="crap321" value="crappy"/>
|
172
|
+
<input type="image" name="form[okay]" id="okay556" value="okay" alt="oh hai thar"/>
|
173
|
+
<button type="submit" id="click_me_123" value="click_me">Click me!</button>
|
174
|
+
<button type="submit" name="form[no_value]">No Value!</button>
|
175
|
+
</p>
|
176
|
+
</form>
|
177
|
+
|
178
|
+
<form id="get-form" action="/form/get?foo=bar" method="get">
|
179
|
+
<p>
|
180
|
+
<label for="form_middle_name">Middle Name</label>
|
181
|
+
<input type="text" name="form[middle_name]" value="Darren" id="form_middle_name"/>
|
182
|
+
</p>
|
183
|
+
|
184
|
+
<p>
|
185
|
+
<input type="submit" name="form[mediocre]" id="mediocre" value="med"/>
|
186
|
+
<p>
|
187
|
+
</form>
|
188
|
+
|
189
|
+
<form action="/upload" method="post" enctype="multipart/form-data">
|
190
|
+
<p>
|
191
|
+
<label for="form_file_name">File Name</label>
|
192
|
+
<input type="file" name="form[file_name]" id="form_file_name"/>
|
193
|
+
</p>
|
194
|
+
|
195
|
+
<p>
|
196
|
+
<label for="form_document">Document</label>
|
197
|
+
<input type="file" name="form[document]" id="form_document"/>
|
198
|
+
</p>
|
199
|
+
|
200
|
+
<p>
|
201
|
+
<input type="submit" value="Upload"/>
|
202
|
+
<p>
|
203
|
+
</form>
|
204
|
+
|
205
|
+
<form action="/redirect" method="post">
|
206
|
+
<p>
|
207
|
+
<input type="submit" value="Go FAR"/>
|
208
|
+
</p>
|
209
|
+
</form>
|
210
|
+
|
211
|
+
<form action="/form" method="post">
|
212
|
+
<p>
|
213
|
+
<label for="html5_email">Html5 Email</label>
|
214
|
+
<input type="email" name="form[html5_email]" value="person@email.com" id="html5_email"/>
|
215
|
+
</p>
|
216
|
+
<p>
|
217
|
+
<label for="html5_url">Html5 Url</label>
|
218
|
+
<input type="url" name="form[html5_url]" value="http://www.example.com" id="html5_url"/>
|
219
|
+
</p>
|
220
|
+
<p>
|
221
|
+
<label for="html5_search">Html5 Search</label>
|
222
|
+
<input type="search" name="form[html5_search]" value="what are you looking for" id="html5_search"/>
|
223
|
+
</p>
|
224
|
+
<p>
|
225
|
+
<label for="html5_tel">Html5 Tel</label>
|
226
|
+
<input type="tel" name="form[html5_tel]" value="911" id="html5_tel"/>
|
227
|
+
</p>
|
228
|
+
<p>
|
229
|
+
<label for="html5_color">Html5 Color</label>
|
230
|
+
<input type="color" name="form[html5_color]" value="#FFF" id="html5_color"/>
|
231
|
+
</p>
|
232
|
+
|
233
|
+
<p>
|
234
|
+
<input type="submit" name="form[html5_submit]" value="html5_submit"/>
|
235
|
+
</p>
|
236
|
+
</form>
|
237
|
+
|
238
|
+
<form action="/form" method="post">
|
239
|
+
<p>
|
240
|
+
<button type="submit" name="form[button]" value="button_first">Just an input that came first</button>
|
241
|
+
<button type="submit" name="form[button]" value="button_second">Just an input</button>
|
242
|
+
<input type="submit" name="form[button]" value="Just a button that came first"/>
|
243
|
+
<input type="submit" name="form[button]" value="Just a button"/>
|
244
|
+
</p>
|
245
|
+
</form>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<div id="bar" title="barDiv">
|
2
|
+
|
3
|
+
</div>
|
4
|
+
|
5
|
+
<div title="noId"></div>
|
6
|
+
|
7
|
+
<div id="foo" title="fooDiv" data="id">
|
8
|
+
<p id="fooDiv">Blah</p>
|
9
|
+
<p>Bax</p>
|
10
|
+
<p title="monkey">Bax</p>
|
11
|
+
<ul><li>A list</li></ul>
|
12
|
+
</div>
|
13
|
+
|
14
|
+
<div id="baz" title="bazDiv"></div>
|
15
|
+
|
16
|
+
<div id="preference">
|
17
|
+
<p id="is-fuzzy">allamas</p>
|
18
|
+
<p id="is-exact">llama</p>
|
19
|
+
</div>
|
20
|
+
|
21
|
+
<p id="whitespace">
|
22
|
+
A lot
|
23
|
+
|
24
|
+
of
|
25
|
+
whitespace
|
26
|
+
</p>
|
@@ -0,0 +1,43 @@
|
|
1
|
+
<h1>This is a test</h1>
|
2
|
+
|
3
|
+
<p id="first">
|
4
|
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
|
5
|
+
tempor incididunt ut <a href="/with_simple_html" title="awesome title" class="simple">labore</a>
|
6
|
+
et dolore magna aliqua. Ut enim ad minim veniam,
|
7
|
+
quis nostrud exercitation <a href="/foo" id="foo">ullamco</a> laboris nisi
|
8
|
+
ut aliquip ex ea commodo consequat.
|
9
|
+
<a href="/with_simple_html"><img src="http://www.foobar.sun/dummy_image.jpg" width="20" height="20" alt="awesome image" /></a>
|
10
|
+
</p>
|
11
|
+
|
12
|
+
<p id="second">
|
13
|
+
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
|
14
|
+
dolore eu fugiat <a href="/redirect" id="red">Redirect</a> pariatur. Excepteur sint occaecat cupidatat non proident,
|
15
|
+
sunt in culpa qui officia
|
16
|
+
text with
|
17
|
+
whitespace
|
18
|
+
id est laborum.
|
19
|
+
</p>
|
20
|
+
|
21
|
+
<p>
|
22
|
+
<input type="text" id="test_field" value="monkey"/>
|
23
|
+
<textarea>banana</textarea>
|
24
|
+
<a href="/redirect_back">BackToMyself</a>
|
25
|
+
<a title="twas a fine link" href="/redirect">A link came first</a>
|
26
|
+
<a title="a fine link" href="/with_simple_html">A link</a>
|
27
|
+
<a title="a fine link with data method" data-method="delete" href="/delete">A link with data-method</a>
|
28
|
+
<a>No Href</a>
|
29
|
+
<a href="">Blank Href</a>
|
30
|
+
<a href="#">Blank Anchor</a>
|
31
|
+
<a href="#anchor">Anchor</a>
|
32
|
+
<a href="/with_simple_html#anchor">Anchor on different page</a>
|
33
|
+
<a href="/with_html#anchor">Anchor on same page</a>
|
34
|
+
<input type="text" value="" id="test_field">
|
35
|
+
<input type="text" checked="checked" id="checked_field">
|
36
|
+
<a href="/redirect"><img src="http://www.foobar.sun/dummy_image.jpg" width="20" height="20" alt="very fine image" /></a>
|
37
|
+
<a href="/with_simple_html"><img src="http://www.foobar.sun/dummy_image.jpg" width="20" height="20" alt="fine image" /></a>
|
38
|
+
</p>
|
39
|
+
|
40
|
+
<div id="hidden" style="display: none;">
|
41
|
+
<div id="hidden_via_ancestor">Inside element with hidden ancestor</div>
|
42
|
+
<a href="/with_simple_html" title="awesome title" class="simple">hidden link</a>
|
43
|
+
</div>
|
data/spec/spec_helper.rb
ADDED
data/spec/union_spec.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe XPath::Union do
|
4
|
+
let(:template) { File.read(File.expand_path('fixtures/simple.html', File.dirname(__FILE__))) }
|
5
|
+
let(:doc) { Nokogiri::HTML(template) }
|
6
|
+
|
7
|
+
describe '#expressions' do
|
8
|
+
it "should return the expressions" do
|
9
|
+
@expr1 = XPath.generate { |x| x.descendant(:p) }
|
10
|
+
@expr2 = XPath.generate { |x| x.descendant(:div) }
|
11
|
+
@collection = XPath::Union.new(@expr1, @expr2)
|
12
|
+
@collection.expressions.should == [@expr1, @expr2]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#each' do
|
17
|
+
it "should iterate through the expressions" do
|
18
|
+
@expr1 = XPath.generate { |x| x.descendant(:p) }
|
19
|
+
@expr2 = XPath.generate { |x| x.descendant(:div) }
|
20
|
+
@collection = XPath::Union.new(@expr1, @expr2)
|
21
|
+
exprs = []
|
22
|
+
@collection.each { |expr| exprs << expr }
|
23
|
+
exprs.should == [@expr1, @expr2]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#map' do
|
28
|
+
it "should map the expressions" do
|
29
|
+
@expr1 = XPath.generate { |x| x.descendant(:p) }
|
30
|
+
@expr2 = XPath.generate { |x| x.descendant(:div) }
|
31
|
+
@collection = XPath::Union.new(@expr1, @expr2)
|
32
|
+
@collection.map { |expr| expr.class }.should == [XPath::Expression::Descendant, XPath::Expression::Descendant]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#to_xpath' do
|
37
|
+
it "should create a valid xpath expression" do
|
38
|
+
@expr1 = XPath.generate { |x| x.descendant(:p) }
|
39
|
+
@expr2 = XPath.generate { |x| x.descendant(:div).where(x.attr(:id) == 'foo') }
|
40
|
+
@collection = XPath::Union.new(@expr1, @expr2)
|
41
|
+
@results = doc.xpath(@collection.to_xpath)
|
42
|
+
@results[0][:title].should == 'fooDiv'
|
43
|
+
@results[1].text.should == 'Blah'
|
44
|
+
@results[2].text.should == 'Bax'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#where, #apply and others' do
|
49
|
+
it "should be delegated to the individual expressions" do
|
50
|
+
@expr1 = XPath.generate { |x| x.descendant(:p) }
|
51
|
+
@expr2 = XPath.generate { |x| x.descendant(:div) }
|
52
|
+
@collection = XPath::Union.new(@expr1, @expr2)
|
53
|
+
@xpath1 = @collection.where(XPath.attr(:id) == 'foo').to_xpath
|
54
|
+
@xpath2 = @collection.where(XPath.attr(:id) == XPath.varstring(:id)).apply(:id => 'fooDiv').to_xpath
|
55
|
+
@results = doc.xpath(@xpath1)
|
56
|
+
@results[0][:title].should == 'fooDiv'
|
57
|
+
@results = doc.xpath(@xpath2)
|
58
|
+
@results[0][:id].should == 'fooDiv'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
data/spec/xpath_spec.rb
ADDED
@@ -0,0 +1,301 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
class Thingy
|
6
|
+
include XPath
|
7
|
+
|
8
|
+
def foo_div
|
9
|
+
descendant(:div).where(attr(:id) == 'foo')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe XPath do
|
14
|
+
let(:template) { File.read(File.expand_path('fixtures/simple.html', File.dirname(__FILE__))) }
|
15
|
+
let(:doc) { Nokogiri::HTML(template) }
|
16
|
+
|
17
|
+
def xpath(predicate=nil, &block)
|
18
|
+
doc.xpath XPath.generate(&block).to_xpath(predicate)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should work as a mixin" do
|
22
|
+
xpath = Thingy.new.foo_div.to_xpath
|
23
|
+
doc.xpath(xpath).first[:title].should == 'fooDiv'
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#descendant' do
|
27
|
+
it "should find nodes that are nested below the current node" do
|
28
|
+
@results = xpath { |x| x.descendant(:p) }
|
29
|
+
@results[0].text.should == "Blah"
|
30
|
+
@results[1].text.should == "Bax"
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should not find nodes outside the context" do
|
34
|
+
@results = xpath do |x|
|
35
|
+
foo_div = x.descendant(:div).where(x.attr(:id) == 'foo')
|
36
|
+
x.descendant(:p).where(x.attr(:id) == foo_div.attr(:title))
|
37
|
+
end
|
38
|
+
@results[0].should be_nil
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should find multiple kinds of nodes" do
|
42
|
+
@results = xpath { |x| x.descendant(:p, :ul) }
|
43
|
+
@results[0].text.should == 'Blah'
|
44
|
+
@results[3].text.should == 'A list'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#child' do
|
49
|
+
it "should find nodes that are nested directly below the current node" do
|
50
|
+
@results = xpath { |x| x.descendant(:div).child(:p) }
|
51
|
+
@results[0].text.should == "Blah"
|
52
|
+
@results[1].text.should == "Bax"
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should not find nodes that are nested further down below the current node" do
|
56
|
+
@results = xpath { |x| x.child(:p) }
|
57
|
+
@results[0].should be_nil
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should find multiple kinds of nodes" do
|
61
|
+
@results = xpath { |x| x.descendant(:div).child(:p, :ul) }
|
62
|
+
@results[0].text.should == 'Blah'
|
63
|
+
@results[3].text.should == 'A list'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '#next_sibling' do
|
68
|
+
it "should find nodes which are immediate siblings of the current node" do
|
69
|
+
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:p) }.first.text.should == 'Bax'
|
70
|
+
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:ul, :p) }.first.text.should == 'Bax'
|
71
|
+
xpath { |x| x.descendant(:p)[x.attr(:title) == 'monkey'].next_sibling(:ul, :p) }.first.text.should == 'A list'
|
72
|
+
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:ul, :li) }.first.should be_nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '#anywhere' do
|
77
|
+
it "should find nodes regardless of the context" do
|
78
|
+
@results = xpath do |x|
|
79
|
+
foo_div = x.anywhere(:div).where(x.attr(:id) == 'foo')
|
80
|
+
x.descendant(:p).where(x.attr(:id) == foo_div.attr(:title))
|
81
|
+
end
|
82
|
+
@results[0].text.should == "Blah"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '#contains' do
|
87
|
+
it "should find nodes that contain the given string" do
|
88
|
+
@results = xpath do |x|
|
89
|
+
x.descendant(:div).where(x.attr(:title).contains('ooD'))
|
90
|
+
end
|
91
|
+
@results[0][:id].should == "foo"
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should find nodes that contain the given expression" do
|
95
|
+
@results = xpath do |x|
|
96
|
+
expression = x.anywhere(:div).where(x.attr(:title) == 'fooDiv').attr(:id)
|
97
|
+
x.descendant(:div).where(x.attr(:title).contains(expression))
|
98
|
+
end
|
99
|
+
@results[0][:id].should == "foo"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe '#text' do
|
104
|
+
it "should select a node's text" do
|
105
|
+
@results = xpath { |x| x.descendant(:p).where(x.text == 'Bax') }
|
106
|
+
@results[0].text.should == 'Bax'
|
107
|
+
@results[1][:title].should == 'monkey'
|
108
|
+
@results = xpath { |x| x.descendant(:div).where(x.descendant(:p).text == 'Bax') }
|
109
|
+
@results[0][:title].should == 'fooDiv'
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe '#where' do
|
114
|
+
it "should limit the expression to find only certain nodes" do
|
115
|
+
xpath { |x| x.descendant(:div).where(:"@id = 'foo'") }.first[:title].should == "fooDiv"
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should be aliased as []" do
|
119
|
+
xpath { |x| x.descendant(:div)[:"@id = 'foo'"] }.first[:title].should == "fooDiv"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe '#inverse' do
|
124
|
+
it "should invert the expression" do
|
125
|
+
xpath { |x| x.descendant(:p).where(x.attr(:id).equals('fooDiv').inverse) }.first.text.should == 'Bax'
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should be aliased as the unary tilde" do
|
129
|
+
xpath { |x| x.descendant(:p).where(~x.attr(:id).equals('fooDiv')) }.first.text.should == 'Bax'
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe '#equals' do
|
134
|
+
it "should limit the expression to find only certain nodes" do
|
135
|
+
xpath { |x| x.descendant(:div).where(x.attr(:id).equals('foo')) }.first[:title].should == "fooDiv"
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should be aliased as ==" do
|
139
|
+
xpath { |x| x.descendant(:div).where(x.attr(:id) == 'foo') }.first[:title].should == "fooDiv"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe '#is' do
|
144
|
+
it "should limit the expression to only nodes that contain the given expression" do
|
145
|
+
@results = xpath { |x| x.descendant(:p).where(x.text.is('llama')) }
|
146
|
+
@results[0][:id].should == 'is-fuzzy'
|
147
|
+
@results[1][:id].should == 'is-exact'
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should limit the expression to only nodes that contain the given expression if fuzzy predicate given" do
|
151
|
+
@results = xpath(:fuzzy) { |x| x.descendant(:p).where(x.text.is('llama')) }
|
152
|
+
@results[0][:id].should == 'is-fuzzy'
|
153
|
+
@results[1][:id].should == 'is-exact'
|
154
|
+
end
|
155
|
+
|
156
|
+
it "should limit the expression to only nodes that equal the given expression if exact predicate given" do
|
157
|
+
@results = xpath(:exact) { |x| x.descendant(:p).where(x.text.is('llama')) }
|
158
|
+
@results[0][:id].should == 'is-exact'
|
159
|
+
@results[1].should be_nil
|
160
|
+
end
|
161
|
+
|
162
|
+
context "with to_xpaths" do
|
163
|
+
it "should prefer exact matches" do
|
164
|
+
@xpath = XPath.generate { |x| x.descendant(:p).where(x.text.is('llama')) }
|
165
|
+
@results = @xpath.to_xpaths.map { |path| doc.xpath(path) }.flatten
|
166
|
+
@results[0][:id].should == 'is-exact'
|
167
|
+
@results[1][:id].should == 'is-fuzzy'
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe '#one_of' do
|
173
|
+
it "should return all nodes where the condition matches" do
|
174
|
+
@results = xpath do |x|
|
175
|
+
p = x.anywhere(:div).where(x.attr(:id) == 'foo').attr(:title)
|
176
|
+
x.descendant(:*).where(x.attr(:id).one_of('foo', p, 'baz'))
|
177
|
+
end
|
178
|
+
@results[0][:title].should == "fooDiv"
|
179
|
+
@results[1].text.should == "Blah"
|
180
|
+
@results[2][:title].should == "bazDiv"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
describe '#and' do
|
185
|
+
it "should find all nodes in both expression" do
|
186
|
+
@results = xpath do |x|
|
187
|
+
x.descendant(:*).where(x.contains('Bax').and(x.attr(:title).equals('monkey')))
|
188
|
+
end
|
189
|
+
@results[0][:title].should == "monkey"
|
190
|
+
end
|
191
|
+
|
192
|
+
it "should be aliased as ampersand (&)" do
|
193
|
+
@results = xpath do |x|
|
194
|
+
x.descendant(:*).where(x.contains('Bax') & x.attr(:title).equals('monkey'))
|
195
|
+
end
|
196
|
+
@results[0][:title].should == "monkey"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
describe '#or' do
|
201
|
+
it "should find all nodes in either expression" do
|
202
|
+
@results = xpath do |x|
|
203
|
+
x.descendant(:*).where(x.attr(:id).equals('foo').or(x.attr(:id).equals('fooDiv')))
|
204
|
+
end
|
205
|
+
@results[0][:title].should == "fooDiv"
|
206
|
+
@results[1].text.should == "Blah"
|
207
|
+
end
|
208
|
+
|
209
|
+
it "should be aliased as pipe (|)" do
|
210
|
+
@results = xpath do |x|
|
211
|
+
x.descendant(:*).where(x.attr(:id).equals('foo') | x.attr(:id).equals('fooDiv'))
|
212
|
+
end
|
213
|
+
@results[0][:title].should == "fooDiv"
|
214
|
+
@results[1].text.should == "Blah"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
describe '#attr' do
|
219
|
+
it "should be an attribute" do
|
220
|
+
@results = xpath { |x| x.descendant(:div).where(x.attr(:id)) }
|
221
|
+
@results[0][:title].should == "barDiv"
|
222
|
+
@results[1][:title].should == "fooDiv"
|
223
|
+
end
|
224
|
+
|
225
|
+
it "should be closed" do
|
226
|
+
@results = xpath do |x|
|
227
|
+
foo_div = x.anywhere(:div).where(x.attr(:id) == 'foo')
|
228
|
+
id = x.attr(foo_div.attr(:data))
|
229
|
+
x.descendant(:div).where(id == 'bar')
|
230
|
+
end.first[:title].should == "barDiv"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
describe '#name' do
|
235
|
+
it "should match the node's name" do
|
236
|
+
xpath { |x| x.descendant(:*).where(x.name == 'ul') }.first.text.should == "A list"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
describe '#apply and #var' do
|
241
|
+
it "should interpolate variables in the xpath expression" do
|
242
|
+
@xpath = XPath.generate do |x|
|
243
|
+
exp = x.descendant(:*).where(x.attr(:id) == x.var(:id).string_literal)
|
244
|
+
end
|
245
|
+
@result1 = doc.xpath(@xpath.apply(:id => 'foo').to_xpath).first
|
246
|
+
@result1[:title].should == 'fooDiv'
|
247
|
+
@result2 = doc.xpath(@xpath.apply(:id => 'baz').to_xpath).first
|
248
|
+
@result2[:title].should == 'bazDiv'
|
249
|
+
end
|
250
|
+
|
251
|
+
it "should raise an argument error if the interpolation key is not given" do
|
252
|
+
@xpath = XPath.generate { |x| x.descendant(:*).where(x.attr(:id) == x.var(:id).string_literal) }
|
253
|
+
lambda { @xpath.apply.to_xpath }.should raise_error(ArgumentError)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
describe '#varstring' do
|
258
|
+
it "should add a literal string variable" do
|
259
|
+
@xpath = XPath.generate { |x| x.descendant(:*).where(x.attr(:id) == x.varstring(:id)) }
|
260
|
+
@result1 = doc.xpath(@xpath.apply(:id => 'foo').to_xpath).first
|
261
|
+
@result1[:title].should == 'fooDiv'
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
describe '#normalize' do
|
266
|
+
it "should normalize whitespace" do
|
267
|
+
xpath { |x| x.descendant(:p).where(x.text.normalize == 'A lot of whitespace') }.first[:id].should == "whitespace"
|
268
|
+
end
|
269
|
+
|
270
|
+
it "should be aliased as 'n'" do
|
271
|
+
xpath { |x| x.descendant(:p).where(x.text.n == 'A lot of whitespace') }.first[:id].should == "whitespace"
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
describe '#union' do
|
276
|
+
it "should create a union expression" do
|
277
|
+
@expr1 = XPath.generate { |x| x.descendant(:p) }
|
278
|
+
@expr2 = XPath.generate { |x| x.descendant(:div) }
|
279
|
+
@collection = @expr1.union(@expr2)
|
280
|
+
@xpath1 = @collection.where(XPath.attr(:id) == 'foo').to_xpath
|
281
|
+
@xpath2 = @collection.where(XPath.attr(:id) == XPath.varstring(:id)).apply(:id => 'fooDiv').to_xpath
|
282
|
+
@results = doc.xpath(@xpath1)
|
283
|
+
@results[0][:title].should == 'fooDiv'
|
284
|
+
@results = doc.xpath(@xpath2)
|
285
|
+
@results[0][:id].should == 'fooDiv'
|
286
|
+
end
|
287
|
+
|
288
|
+
it "should be aliased as +" do
|
289
|
+
@expr1 = XPath.generate { |x| x.descendant(:p) }
|
290
|
+
@expr2 = XPath.generate { |x| x.descendant(:div) }
|
291
|
+
@collection = @expr1 + @expr2
|
292
|
+
@xpath1 = @collection.where(XPath.attr(:id) == 'foo').to_xpath
|
293
|
+
@xpath2 = @collection.where(XPath.attr(:id) == XPath.varstring(:id)).apply(:id => 'fooDiv').to_xpath
|
294
|
+
@results = doc.xpath(@xpath1)
|
295
|
+
@results[0][:title].should == 'fooDiv'
|
296
|
+
@results = doc.xpath(@xpath2)
|
297
|
+
@results[0][:id].should == 'fooDiv'
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
end
|
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xpath
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Jonas Nicklas
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-08-15 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: rspec
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 13
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 2
|
33
|
+
- 9
|
34
|
+
version: 1.2.9
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: nokogiri
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 29
|
46
|
+
segments:
|
47
|
+
- 1
|
48
|
+
- 3
|
49
|
+
- 3
|
50
|
+
version: 1.3.3
|
51
|
+
type: :development
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: yard
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 27
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
- 5
|
65
|
+
- 8
|
66
|
+
version: 0.5.8
|
67
|
+
type: :development
|
68
|
+
version_requirements: *id003
|
69
|
+
description: XPath is a Ruby DSL for generating XPath expressions
|
70
|
+
email:
|
71
|
+
- jonas.nicklas@gmail.com
|
72
|
+
executables: []
|
73
|
+
|
74
|
+
extensions: []
|
75
|
+
|
76
|
+
extra_rdoc_files:
|
77
|
+
- README.rdoc
|
78
|
+
files:
|
79
|
+
- lib/xpath/expression.rb
|
80
|
+
- lib/xpath/html.rb
|
81
|
+
- lib/xpath/union.rb
|
82
|
+
- lib/xpath/version.rb
|
83
|
+
- lib/xpath.rb
|
84
|
+
- spec/fixtures/form.html
|
85
|
+
- spec/fixtures/simple.html
|
86
|
+
- spec/fixtures/stuff.html
|
87
|
+
- spec/spec_helper.rb
|
88
|
+
- spec/union_spec.rb
|
89
|
+
- spec/xpath_spec.rb
|
90
|
+
- README.rdoc
|
91
|
+
has_rdoc: true
|
92
|
+
homepage: http://github.com/jnicklas/xpath
|
93
|
+
licenses: []
|
94
|
+
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options:
|
97
|
+
- --main
|
98
|
+
- README.rdoc
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
hash: 3
|
107
|
+
segments:
|
108
|
+
- 0
|
109
|
+
version: "0"
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
hash: 3
|
116
|
+
segments:
|
117
|
+
- 0
|
118
|
+
version: "0"
|
119
|
+
requirements: []
|
120
|
+
|
121
|
+
rubyforge_project: xpath
|
122
|
+
rubygems_version: 1.3.7
|
123
|
+
signing_key:
|
124
|
+
specification_version: 3
|
125
|
+
summary: Generate XPath expressions from Ruby
|
126
|
+
test_files: []
|
127
|
+
|