wgibbs-xpath 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +56 -0
- data/lib/xpath.rb +68 -0
- data/lib/xpath/expression.rb +310 -0
- data/lib/xpath/html.rb +114 -0
- data/lib/xpath/union.rb +27 -0
- data/lib/xpath/version.rb +3 -0
- data/spec/fixtures/form.html +87 -0
- data/spec/fixtures/simple.html +32 -0
- data/spec/fixtures/stuff.html +43 -0
- data/spec/html_spec.rb +187 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/union_spec.rb +63 -0
- data/spec/xpath_spec.rb +353 -0
- metadata +129 -0
data/spec/xpath_spec.rb
ADDED
@@ -0,0 +1,353 @@
|
|
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
|
+
|
47
|
+
it "should find all nodes when no arguments given" do
|
48
|
+
@results = xpath { |x| x.descendant[x.attr(:id) == 'foo'].descendant }
|
49
|
+
@results[0].text.should == 'Blah'
|
50
|
+
@results[4].text.should == 'A list'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#child' do
|
55
|
+
it "should find nodes that are nested directly below the current node" do
|
56
|
+
@results = xpath { |x| x.descendant(:div).child(:p) }
|
57
|
+
@results[0].text.should == "Blah"
|
58
|
+
@results[1].text.should == "Bax"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should not find nodes that are nested further down below the current node" do
|
62
|
+
@results = xpath { |x| x.child(:p) }
|
63
|
+
@results[0].should be_nil
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should find multiple kinds of nodes" do
|
67
|
+
@results = xpath { |x| x.descendant(:div).child(:p, :ul) }
|
68
|
+
@results[0].text.should == 'Blah'
|
69
|
+
@results[3].text.should == 'A list'
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should find all nodes when no arguments given" do
|
73
|
+
@results = xpath { |x| x.descendant[x.attr(:id) == 'foo'].child }
|
74
|
+
@results[0].text.should == 'Blah'
|
75
|
+
@results[3].text.should == 'A list'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe '#next_sibling' do
|
80
|
+
it "should find nodes which are immediate siblings of the current node" do
|
81
|
+
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:p) }.first.text.should == 'Bax'
|
82
|
+
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:ul, :p) }.first.text.should == 'Bax'
|
83
|
+
xpath { |x| x.descendant(:p)[x.attr(:title) == 'monkey'].next_sibling(:ul, :p) }.first.text.should == 'A list'
|
84
|
+
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:ul, :li) }.first.should be_nil
|
85
|
+
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling }.first.text.should == 'Bax'
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe '#anywhere' do
|
90
|
+
it "should find nodes regardless of the context" do
|
91
|
+
@results = xpath do |x|
|
92
|
+
foo_div = x.anywhere(:div).where(x.attr(:id) == 'foo')
|
93
|
+
x.descendant(:p).where(x.attr(:id) == foo_div.attr(:title))
|
94
|
+
end
|
95
|
+
@results[0].text.should == "Blah"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe '#tag' do
|
100
|
+
it "should filter elements by tag" do
|
101
|
+
@results = xpath { |x| x.descendant[x.tag(:p) | x.tag(:li)] }
|
102
|
+
@results[0].text.should == 'Blah'
|
103
|
+
@results[3].text.should == 'A list'
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe '#contains' do
|
108
|
+
it "should find nodes that contain the given string" do
|
109
|
+
@results = xpath do |x|
|
110
|
+
x.descendant(:div).where(x.attr(:title).contains('ooD'))
|
111
|
+
end
|
112
|
+
@results[0][:id].should == "foo"
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should find nodes that contain the given expression" do
|
116
|
+
@results = xpath do |x|
|
117
|
+
expression = x.anywhere(:div).where(x.attr(:title) == 'fooDiv').attr(:id)
|
118
|
+
x.descendant(:div).where(x.attr(:title).contains(expression))
|
119
|
+
end
|
120
|
+
@results[0][:id].should == "foo"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe '#text' do
|
125
|
+
it "should select a node's text" do
|
126
|
+
@results = xpath { |x| x.descendant(:p).where(x.text == 'Bax') }
|
127
|
+
@results[0].text.should == 'Bax'
|
128
|
+
@results[1][:title].should == 'monkey'
|
129
|
+
@results = xpath { |x| x.descendant(:div).where(x.descendant(:p).text == 'Bax') }
|
130
|
+
@results[0][:title].should == 'fooDiv'
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe '#where' do
|
135
|
+
it "should limit the expression to find only certain nodes" do
|
136
|
+
xpath { |x| x.descendant(:div).where(:"@id = 'foo'") }.first[:title].should == "fooDiv"
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should be aliased as []" do
|
140
|
+
xpath { |x| x.descendant(:div)[:"@id = 'foo'"] }.first[:title].should == "fooDiv"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe '#inverse' do
|
145
|
+
it "should invert the expression" do
|
146
|
+
xpath { |x| x.descendant(:p).where(x.attr(:id).equals('fooDiv').inverse) }.first.text.should == 'Bax'
|
147
|
+
end
|
148
|
+
|
149
|
+
it "should be aliased as the unary tilde" do
|
150
|
+
xpath { |x| x.descendant(:p).where(~x.attr(:id).equals('fooDiv')) }.first.text.should == 'Bax'
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe '#equals' do
|
155
|
+
it "should limit the expression to find only certain nodes" do
|
156
|
+
xpath { |x| x.descendant(:div).where(x.attr(:id).equals('foo')) }.first[:title].should == "fooDiv"
|
157
|
+
end
|
158
|
+
|
159
|
+
it "should be aliased as ==" do
|
160
|
+
xpath { |x| x.descendant(:div).where(x.attr(:id) == 'foo') }.first[:title].should == "fooDiv"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe '#is' do
|
165
|
+
it "should limit the expression to only nodes that contain the given expression" do
|
166
|
+
@results = xpath { |x| x.descendant(:p).where(x.text.is('llama')) }
|
167
|
+
@results[0][:id].should == 'is-fuzzy'
|
168
|
+
@results[1][:id].should == 'is-exact'
|
169
|
+
end
|
170
|
+
|
171
|
+
it "should limit the expression to only nodes that contain the given expression if fuzzy predicate given" do
|
172
|
+
@results = xpath(:fuzzy) { |x| x.descendant(:p).where(x.text.is('llama')) }
|
173
|
+
@results[0][:id].should == 'is-fuzzy'
|
174
|
+
@results[1][:id].should == 'is-exact'
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should limit the expression to only nodes that equal the given expression if exact predicate given" do
|
178
|
+
@results = xpath(:exact) { |x| x.descendant(:p).where(x.text.is('llama')) }
|
179
|
+
@results[0][:id].should == 'is-exact'
|
180
|
+
@results[1].should be_nil
|
181
|
+
end
|
182
|
+
|
183
|
+
context "with to_xpaths" do
|
184
|
+
it "should prefer exact matches" do
|
185
|
+
@xpath = XPath.generate { |x| x.descendant(:p).where(x.text.is('llama')) }
|
186
|
+
@results = @xpath.to_xpaths.map { |path| doc.xpath(path) }.flatten
|
187
|
+
@results[0][:id].should == 'is-exact'
|
188
|
+
@results[1][:id].should == 'is-fuzzy'
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
describe '#one_of' do
|
194
|
+
it "should return all nodes where the condition matches" do
|
195
|
+
@results = xpath do |x|
|
196
|
+
p = x.anywhere(:div).where(x.attr(:id) == 'foo').attr(:title)
|
197
|
+
x.descendant(:*).where(x.attr(:id).one_of('foo', p, 'baz'))
|
198
|
+
end
|
199
|
+
@results[0][:title].should == "fooDiv"
|
200
|
+
@results[1].text.should == "Blah"
|
201
|
+
@results[2][:title].should == "bazDiv"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe '#and' do
|
206
|
+
it "should find all nodes in both expression" do
|
207
|
+
@results = xpath do |x|
|
208
|
+
x.descendant(:*).where(x.contains('Bax').and(x.attr(:title).equals('monkey')))
|
209
|
+
end
|
210
|
+
@results[0][:title].should == "monkey"
|
211
|
+
end
|
212
|
+
|
213
|
+
it "should be aliased as ampersand (&)" do
|
214
|
+
@results = xpath do |x|
|
215
|
+
x.descendant(:*).where(x.contains('Bax') & x.attr(:title).equals('monkey'))
|
216
|
+
end
|
217
|
+
@results[0][:title].should == "monkey"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
describe '#or' do
|
222
|
+
it "should find all nodes in either expression" do
|
223
|
+
@results = xpath do |x|
|
224
|
+
x.descendant(:*).where(x.attr(:id).equals('foo').or(x.attr(:id).equals('fooDiv')))
|
225
|
+
end
|
226
|
+
@results[0][:title].should == "fooDiv"
|
227
|
+
@results[1].text.should == "Blah"
|
228
|
+
end
|
229
|
+
|
230
|
+
it "should be aliased as pipe (|)" do
|
231
|
+
@results = xpath do |x|
|
232
|
+
x.descendant(:*).where(x.attr(:id).equals('foo') | x.attr(:id).equals('fooDiv'))
|
233
|
+
end
|
234
|
+
@results[0][:title].should == "fooDiv"
|
235
|
+
@results[1].text.should == "Blah"
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
describe '#attr' do
|
240
|
+
it "should be an attribute" do
|
241
|
+
@results = xpath { |x| x.descendant(:div).where(x.attr(:id)) }
|
242
|
+
@results[0][:title].should == "barDiv"
|
243
|
+
@results[1][:title].should == "fooDiv"
|
244
|
+
end
|
245
|
+
|
246
|
+
it "should be closed" do
|
247
|
+
@results = xpath do |x|
|
248
|
+
foo_div = x.anywhere(:div).where(x.attr(:id) == 'foo')
|
249
|
+
id = x.attr(foo_div.attr(:data))
|
250
|
+
x.descendant(:div).where(id == 'bar')
|
251
|
+
end.first[:title].should == "barDiv"
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
describe '#css' do
|
256
|
+
it "should find nodes by the given CSS selector" do
|
257
|
+
@results = xpath { |x| x.css('#preference p') }
|
258
|
+
@results[0].text.should == 'allamas'
|
259
|
+
@results[1].text.should == 'llama'
|
260
|
+
end
|
261
|
+
|
262
|
+
it "should respect previous expression" do
|
263
|
+
@results = xpath { |x| x.descendant[x.attr(:id) == 'moar'].css('p') }
|
264
|
+
@results[0].text.should == 'chimp'
|
265
|
+
@results[1].text.should == 'flamingo'
|
266
|
+
end
|
267
|
+
|
268
|
+
it "should be composable" do
|
269
|
+
@results = xpath { |x| x.css('#moar').descendant(:p) }
|
270
|
+
@results[0].text.should == 'chimp'
|
271
|
+
@results[1].text.should == 'flamingo'
|
272
|
+
end
|
273
|
+
|
274
|
+
it "should allow comma separated selectors" do
|
275
|
+
@results = xpath { |x| x.descendant[x.attr(:id) == 'moar'].css('div, p') }
|
276
|
+
@results[0].text.should == 'chimp'
|
277
|
+
@results[1].text.should == 'elephant'
|
278
|
+
@results[2].text.should == 'flamingo'
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
describe '#name' do
|
283
|
+
it "should match the node's name" do
|
284
|
+
xpath { |x| x.descendant(:*).where(x.name == 'ul') }.first.text.should == "A list"
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
describe '#apply and #var' do
|
289
|
+
it "should interpolate variables in the xpath expression" do
|
290
|
+
@xpath = XPath.generate do |x|
|
291
|
+
exp = x.descendant(:*).where(x.attr(:id) == x.var(:id).string_literal)
|
292
|
+
end
|
293
|
+
@result1 = doc.xpath(@xpath.apply(:id => 'foo').to_xpath).first
|
294
|
+
@result1[:title].should == 'fooDiv'
|
295
|
+
@result2 = doc.xpath(@xpath.apply(:id => 'baz').to_xpath).first
|
296
|
+
@result2[:title].should == 'bazDiv'
|
297
|
+
end
|
298
|
+
|
299
|
+
it "should raise an argument error if the interpolation key is not given" do
|
300
|
+
@xpath = XPath.generate { |x| x.descendant(:*).where(x.attr(:id) == x.var(:id).string_literal) }
|
301
|
+
if defined?(KeyError)
|
302
|
+
lambda { @xpath.apply.to_xpath }.should raise_error(KeyError)
|
303
|
+
else
|
304
|
+
lambda { @xpath.apply.to_xpath }.should raise_error(ArgumentError)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
describe '#varstring' do
|
310
|
+
it "should add a literal string variable" do
|
311
|
+
@xpath = XPath.generate { |x| x.descendant(:*).where(x.attr(:id) == x.varstring(:id)) }
|
312
|
+
@result1 = doc.xpath(@xpath.apply(:id => 'foo').to_xpath).first
|
313
|
+
@result1[:title].should == 'fooDiv'
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
describe '#normalize' do
|
318
|
+
it "should normalize whitespace" do
|
319
|
+
xpath { |x| x.descendant(:p).where(x.text.normalize == 'A lot of whitespace') }.first[:id].should == "whitespace"
|
320
|
+
end
|
321
|
+
|
322
|
+
it "should be aliased as 'n'" do
|
323
|
+
xpath { |x| x.descendant(:p).where(x.text.n == 'A lot of whitespace') }.first[:id].should == "whitespace"
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
describe '#union' do
|
328
|
+
it "should create a union expression" do
|
329
|
+
@expr1 = XPath.generate { |x| x.descendant(:p) }
|
330
|
+
@expr2 = XPath.generate { |x| x.descendant(:div) }
|
331
|
+
@collection = @expr1.union(@expr2)
|
332
|
+
@xpath1 = @collection.where(XPath.attr(:id) == 'foo').to_xpath
|
333
|
+
@xpath2 = @collection.where(XPath.attr(:id) == XPath.varstring(:id)).apply(:id => 'fooDiv').to_xpath
|
334
|
+
@results = doc.xpath(@xpath1)
|
335
|
+
@results[0][:title].should == 'fooDiv'
|
336
|
+
@results = doc.xpath(@xpath2)
|
337
|
+
@results[0][:id].should == 'fooDiv'
|
338
|
+
end
|
339
|
+
|
340
|
+
it "should be aliased as +" do
|
341
|
+
@expr1 = XPath.generate { |x| x.descendant(:p) }
|
342
|
+
@expr2 = XPath.generate { |x| x.descendant(:div) }
|
343
|
+
@collection = @expr1 + @expr2
|
344
|
+
@xpath1 = @collection.where(XPath.attr(:id) == 'foo').to_xpath
|
345
|
+
@xpath2 = @collection.where(XPath.attr(:id) == XPath.varstring(:id)).apply(:id => 'fooDiv').to_xpath
|
346
|
+
@results = doc.xpath(@xpath1)
|
347
|
+
@results[0][:title].should == 'fooDiv'
|
348
|
+
@results = doc.xpath(@xpath2)
|
349
|
+
@results[0][:id].should == 'fooDiv'
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
end
|
metadata
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wgibbs-xpath
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 19
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 4
|
10
|
+
version: 0.1.4
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Wes Gibbs
|
14
|
+
- Jonas Nicklas
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2011-03-09 00:00:00 -05:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: nokogiri
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ~>
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 9
|
31
|
+
segments:
|
32
|
+
- 1
|
33
|
+
- 3
|
34
|
+
version: "1.3"
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rspec
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 13
|
46
|
+
segments:
|
47
|
+
- 1
|
48
|
+
- 2
|
49
|
+
- 9
|
50
|
+
version: 1.2.9
|
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
|
+
- wesgibbs@gmail.com
|
72
|
+
- jonas.nicklas@gmail.com
|
73
|
+
executables: []
|
74
|
+
|
75
|
+
extensions: []
|
76
|
+
|
77
|
+
extra_rdoc_files:
|
78
|
+
- README.rdoc
|
79
|
+
files:
|
80
|
+
- lib/xpath/expression.rb
|
81
|
+
- lib/xpath/html.rb
|
82
|
+
- lib/xpath/union.rb
|
83
|
+
- lib/xpath/version.rb
|
84
|
+
- lib/xpath.rb
|
85
|
+
- spec/fixtures/form.html
|
86
|
+
- spec/fixtures/simple.html
|
87
|
+
- spec/fixtures/stuff.html
|
88
|
+
- spec/html_spec.rb
|
89
|
+
- spec/spec_helper.rb
|
90
|
+
- spec/union_spec.rb
|
91
|
+
- spec/xpath_spec.rb
|
92
|
+
- README.rdoc
|
93
|
+
has_rdoc: true
|
94
|
+
homepage: http://github.com/wgibbs/xpath
|
95
|
+
licenses: []
|
96
|
+
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options:
|
99
|
+
- --main
|
100
|
+
- README.rdoc
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
none: false
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
hash: 3
|
109
|
+
segments:
|
110
|
+
- 0
|
111
|
+
version: "0"
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
hash: 3
|
118
|
+
segments:
|
119
|
+
- 0
|
120
|
+
version: "0"
|
121
|
+
requirements: []
|
122
|
+
|
123
|
+
rubyforge_project: wgibbs-xpath
|
124
|
+
rubygems_version: 1.5.2
|
125
|
+
signing_key:
|
126
|
+
specification_version: 3
|
127
|
+
summary: Generate XPath expressions from Ruby
|
128
|
+
test_files: []
|
129
|
+
|