xpath 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.
@@ -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
+
@@ -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
@@ -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,3 @@
1
+ module XPath
2
+ VERSION = '0.1.0'
3
+ 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>
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'xpath'
@@ -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
+
@@ -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
+