xpath 0.1.0

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