xpath 0.1.4 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,15 +1,21 @@
1
1
  = XPath
2
2
 
3
- XPath is a Ruby DSL around a subset of XPath 1.0. It's primary purpose is to
3
+ XPath is a Ruby DSL around a subset of XPath 1.0. Its primary purpose is to
4
4
  facilitate writing complex XPath queries from Ruby code.
5
5
 
6
+ {<img src="http://travis-ci.org/jnicklas/xpath.png" />}[http://travis-ci.org/jnicklas/xpath]
7
+
6
8
  == Generating expressions
7
9
 
8
- To create quick, one of expressions, XPath.generate can be used:
10
+ To create quick, one-off expressions, +XPath.generate+ can be used:
9
11
 
10
12
  XPath.generate { |x| x.descendant(:ul)[x.attr(:id) == 'foo'] }
11
13
 
12
- However for more complex expressions, it is probably ore convenient to include
14
+ You can also call expression methods directly on the XPath module:
15
+
16
+ XPath.descendant(:ul)[XPath.attr(:id) == 'foo'] }
17
+
18
+ However for more complex expressions, it is probably more convenient to include
13
19
  the XPath module into your own class or module:
14
20
 
15
21
  module MyXPaths
@@ -25,12 +31,63 @@ the XPath module into your own class or module:
25
31
  end
26
32
 
27
33
  Both ways return an XPath::Expression instance, which can be further modified.
28
- To convert the expression to a string, just call #to_s on it.
34
+ To convert the expression to a string, just call #to_s on it. All available
35
+ expressions are defined in XPath::DSL.
36
+
37
+ == String, Hashes and Symbols
38
+
39
+ When you send a string as an argument to any XPath function, XPath assumes this
40
+ to be a string literal. On the other hand if you send in Symbol, XPath assumes
41
+ this to be an XPath literal. Thus the following two statements are not
42
+ equivalent:
43
+
44
+ XPath.descendant(:p)[XPath.attr(:id) == 'foo']
45
+ XPath.descendant(:p)[XPath.attr(:id) == :foo]
46
+
47
+ These are the XPath expressions that these would be translated to:
48
+
49
+ .//p[@id = 'foo']
50
+ .//p[@id = foo]
51
+
52
+ The second expression would match any p tag whose id attribute matches a 'foo'
53
+ tag it contains. Most likely this is not what you want.
54
+
55
+ In fact anything other than a String is treated as a literal. Thus the
56
+ following works as expected:
57
+
58
+ XPath.descendant(:p)[1]
59
+
60
+ Keep in mind that XPath is 1-indexed and not 0-indexed like most other
61
+ programming languages, including Ruby.
62
+
63
+ Hashes are automatically converted to equality expressions, so the above
64
+ example could be written as:
65
+
66
+ XPath.descendant(:p)[:@id => 'foo']
67
+
68
+ Which would generate the same expression:
69
+
70
+ .//p[@id = 'foo']
71
+
72
+ Note that the same rules apply here, both the keys and values in the hash are
73
+ treated the same way as any other expression in XPath. Thus the following are
74
+ not equivalent:
75
+
76
+ XPath.descendant(:p)[:@id => 'foo'] # => .//p[@id = 'foo']
77
+ XPath.descendant(:p)[:id => 'foo'] # => .//p[id = 'foo']
78
+ XPath.descendant(:p)['id' => 'foo'] # => .//p['id' = 'foo']
29
79
 
30
80
  == HTML
31
81
 
32
82
  XPath comes with a set of premade XPaths for use with HTML documents.
33
83
 
84
+ You can generate these like this:
85
+
86
+ XPath::HTML.link('Home')
87
+ XPath::HTML.field('Name')
88
+
89
+ See XPath::HTML for all available matchers.
90
+
34
91
  == License
35
92
 
36
93
  (The MIT License)
data/lib/xpath.rb CHANGED
@@ -2,67 +2,16 @@ require 'nokogiri'
2
2
 
3
3
  module XPath
4
4
  autoload :Expression, 'xpath/expression'
5
+ autoload :Literal, 'xpath/literal'
5
6
  autoload :Union, 'xpath/union'
7
+ autoload :Renderer, 'xpath/renderer'
6
8
  autoload :HTML, 'xpath/html'
9
+ autoload :DSL, 'xpath/dsl'
7
10
 
8
- extend self
11
+ extend XPath::DSL::TopLevel
12
+ include XPath::DSL::TopLevel
9
13
 
10
14
  def self.generate
11
- yield(Expression::Self.new)
12
- end
13
-
14
- def current
15
- Expression::Self.new
16
- end
17
-
18
- def name
19
- Expression::Name.new(current)
20
- end
21
-
22
- def descendant(*expressions)
23
- Expression::Descendant.new(current, expressions)
24
- end
25
-
26
- def child(*expressions)
27
- Expression::Child.new(current, expressions)
28
- end
29
-
30
- def anywhere(expression)
31
- Expression::Anywhere.new(expression)
32
- end
33
-
34
- def attr(expression)
35
- Expression::Attribute.new(current, expression)
36
- end
37
-
38
- def contains(expression)
39
- Expression::Contains.new(current, expression)
40
- end
41
-
42
- def text
43
- Expression::Text.new(current)
44
- end
45
-
46
- def var(name)
47
- Expression::Variable.new(name)
48
- end
49
-
50
- def string
51
- Expression::StringFunction.new(current)
52
- end
53
-
54
- def tag(name)
55
- Expression::Tag.new(name)
56
- end
57
-
58
- def css(selector)
59
- paths = Nokogiri::CSS.xpath_for(selector).map do |selector|
60
- Expression::CSS.new(current, Expression::Literal.new(selector))
61
- end
62
- Union.new(*paths)
63
- end
64
-
65
- def varstring(name)
66
- var(name).string_literal
15
+ yield(self)
67
16
  end
68
17
  end
data/lib/xpath/dsl.rb ADDED
@@ -0,0 +1,104 @@
1
+ module XPath
2
+ module DSL
3
+ module TopLevel
4
+ def current
5
+ Expression.new(:this_node)
6
+ end
7
+
8
+ def name
9
+ Expression.new(:node_name, current)
10
+ end
11
+
12
+ def descendant(*expressions)
13
+ Expression.new(:descendant, current, expressions)
14
+ end
15
+
16
+ def child(*expressions)
17
+ Expression.new(:child, current, expressions)
18
+ end
19
+
20
+ def axis(name, tag_name=:*)
21
+ Expression.new(:axis, current, name, tag_name)
22
+ end
23
+
24
+ def anywhere(expression)
25
+ Expression.new(:anywhere, expression)
26
+ end
27
+
28
+ def attr(expression)
29
+ Expression.new(:attribute, current, expression)
30
+ end
31
+
32
+ def contains(expression)
33
+ Expression.new(:contains, current, expression)
34
+ end
35
+
36
+ def starts_with(expression)
37
+ Expression.new(:starts_with, current, expression)
38
+ end
39
+
40
+ def text
41
+ Expression.new(:text, current)
42
+ end
43
+
44
+ def string
45
+ Expression.new(:string_function, current)
46
+ end
47
+
48
+ def css(selector)
49
+ Expression.new(:css, current, Literal.new(selector))
50
+ end
51
+ end
52
+
53
+ module ExpressionLevel
54
+ include XPath::DSL::TopLevel
55
+
56
+ def where(expression)
57
+ Expression.new(:where, current, expression)
58
+ end
59
+ alias_method :[], :where
60
+
61
+ def next_sibling(*expressions)
62
+ Expression.new(:next_sibling, current, expressions)
63
+ end
64
+
65
+ def one_of(*expressions)
66
+ Expression.new(:one_of, current, expressions)
67
+ end
68
+
69
+ def equals(expression)
70
+ Expression.new(:equality, current, expression)
71
+ end
72
+ alias_method :==, :equals
73
+
74
+ def or(expression)
75
+ Expression.new(:or, current, expression)
76
+ end
77
+ alias_method :|, :or
78
+
79
+ def and(expression)
80
+ Expression.new(:and, current, expression)
81
+ end
82
+ alias_method :&, :and
83
+
84
+ def union(*expressions)
85
+ Union.new(*[self, expressions].flatten)
86
+ end
87
+ alias_method :+, :union
88
+
89
+ def inverse
90
+ Expression.new(:inverse, current)
91
+ end
92
+ alias_method :~, :inverse
93
+
94
+ def string_literal
95
+ Expression.new(:string_literal, self)
96
+ end
97
+
98
+ def normalize
99
+ Expression.new(:normalized_space, current)
100
+ end
101
+ alias_method :n, :normalize
102
+ end
103
+ end
104
+ end
@@ -1,310 +1,20 @@
1
1
  module XPath
2
2
  class Expression
3
- include XPath
3
+ attr_accessor :expression, :arguments
4
+ include XPath::DSL::ExpressionLevel
4
5
 
5
- class Self < Expression
6
- def to_xpath(predicate=nil)
7
- '.'
8
- end
9
- end
10
-
11
- class Unary < Expression
12
- def initialize(expression)
13
- @expression = wrap_xpath(expression)
14
- end
15
- end
16
-
17
- class Binary < Expression
18
- def initialize(left, right)
19
- @left = wrap_xpath(left)
20
- @right = wrap_xpath(right)
21
- end
22
- end
23
-
24
- class Multiple < Expression
25
- def initialize(left, expressions)
26
- @left = wrap_xpath(left)
27
- @expressions = expressions.map { |e| wrap_xpath(e) }
28
- end
29
- end
30
-
31
- class Literal < Expression
32
- def initialize(expression)
33
- @expression = expression
34
- end
35
-
36
- def to_xpath(predicate=nil)
37
- @expression.to_s
38
- end
39
- end
40
-
41
- class Child < Multiple
42
- def to_xpath(predicate=nil)
43
- if @expressions.length == 1
44
- "#{@left.to_xpath(predicate)}/#{@expressions.first.to_xpath(predicate)}"
45
- elsif @expressions.length > 1
46
- "#{@left.to_xpath(predicate)}/*[#{@expressions.map { |e| "self::#{e.to_xpath(predicate)}" }.join(" | ")}]"
47
- else
48
- "#{@left.to_xpath(predicate)}/*"
49
- end
50
- end
51
- end
52
-
53
- class Descendant < Multiple
54
- def to_xpath(predicate=nil)
55
- if @expressions.length == 1
56
- "#{@left.to_xpath(predicate)}//#{@expressions.first.to_xpath(predicate)}"
57
- elsif @expressions.length > 1
58
- "#{@left.to_xpath(predicate)}//*[#{@expressions.map { |e| "self::#{e.to_xpath(predicate)}" }.join(" | ")}]"
59
- else
60
- "#{@left.to_xpath(predicate)}//*"
61
- end
62
- end
63
- end
64
-
65
- class NextSibling < Multiple
66
- def to_xpath(predicate=nil)
67
- if @expressions.length == 1
68
- "#{@left.to_xpath(predicate)}/following-sibling::*[1]/self::#{@expressions.first.to_xpath(predicate)}"
69
- elsif @expressions.length > 1
70
- "#{@left.to_xpath(predicate)}/following-sibling::*[1]/self::*[#{@expressions.map { |e| "self::#{e.to_xpath(predicate)}" }.join(" | ")}]"
71
- else
72
- "#{@left.to_xpath(predicate)}/following-sibling::*[1]/self::*"
73
- end
74
- end
75
- end
76
-
77
- class Tag < Unary
78
- def to_xpath(predicate=nil)
79
- "self::#{@expression.to_xpath(predicate)}"
80
- end
81
- end
82
-
83
- class Anywhere < Unary
84
- def to_xpath(predicate=nil)
85
- "//#{@expression.to_xpath(predicate)}"
86
- end
87
- end
88
-
89
- class Name < Unary
90
- def to_xpath(predicate=nil)
91
- "name(#{@expression.to_xpath(predicate)})"
92
- end
93
- end
94
-
95
- class Where < Binary
96
- def to_xpath(predicate=nil)
97
- "#{@left.to_xpath(predicate)}[#{@right.to_xpath(predicate)}]"
98
- end
99
- end
100
-
101
- class Attribute < Binary
102
- def to_xpath(predicate=nil)
103
- if @right.is_a?(Literal)
104
- "#{@left.to_xpath(predicate)}/@#{@right.to_xpath(predicate)}"
105
- else
106
- "#{@left.to_xpath(predicate)}/attribute::node()[name(.) = #{@right.to_xpath(predicate)}]"
107
- end
108
- end
109
- end
110
-
111
- class Equality < Binary
112
- def to_xpath(predicate=nil)
113
- "#{@left.to_xpath(predicate)} = #{@right.to_xpath(predicate)}"
114
- end
115
- end
116
-
117
- class StringFunction < Unary
118
- def to_xpath(predicate=nil)
119
- "string(#{@expression.to_xpath(predicate)})"
120
- end
121
- end
122
-
123
- class StringLiteral < Expression
124
- def initialize(expression)
125
- @expression = expression
126
- end
127
-
128
- def to_xpath(predicate=nil)
129
- string = @expression
130
- string = @expression.to_xpath(predicate) unless @expression.is_a?(String)
131
- if string.include?("'")
132
- string = string.split("'", -1).map do |substr|
133
- "'#{substr}'"
134
- end.join(%q{,"'",})
135
- "concat(#{string})"
136
- else
137
- "'#{string}'"
138
- end
139
- end
140
- end
141
-
142
- class NormalizedSpace < Unary
143
- def to_xpath(predicate=nil)
144
- "normalize-space(#{@expression.to_xpath(predicate)})"
145
- end
146
- end
147
-
148
- class And < Binary
149
- def to_xpath(predicate=nil)
150
- "(#{@left.to_xpath(predicate)} and #{@right.to_xpath(predicate)})"
151
- end
152
- end
153
-
154
- class Or < Binary
155
- def to_xpath(predicate=nil)
156
- "(#{@left.to_xpath(predicate)} or #{@right.to_xpath(predicate)})"
157
- end
158
- end
159
-
160
- class OneOf < Expression
161
- def initialize(left, right)
162
- @left = wrap_xpath(left)
163
- @right = right.map { |r| wrap_xpath(r) }
164
- end
165
-
166
- def to_xpath(predicate=nil)
167
- @right.map { |r| "#{@left.to_xpath(predicate)} = #{r.to_xpath(predicate)}" }.join(' or ')
168
- end
169
- end
170
-
171
- class Contains < Binary
172
- def to_xpath(predicate=nil)
173
- "contains(#{@left.to_xpath(predicate)}, #{@right.to_xpath(predicate)})"
174
- end
175
- end
176
-
177
- class Is < Binary
178
- def to_xpath(predicate=nil)
179
- if predicate == :exact
180
- Equality.new(@left, @right).to_xpath(predicate)
181
- else
182
- Contains.new(@left, @right).to_xpath(predicate)
183
- end
184
- end
185
- end
186
-
187
- class Text < Unary
188
- def to_xpath(predicate=nil)
189
- "#{@expression.to_xpath(predicate)}/text()"
190
- end
191
- end
192
-
193
- class Variable < Expression
194
- def initialize(name)
195
- @name = name
196
- end
197
-
198
- def to_xpath(predicate=nil)
199
- "%{#{@name}}"
200
- end
201
- end
202
-
203
- class Inverse < Unary
204
- def to_xpath(predicate=nil)
205
- "not(#{@expression.to_xpath(predicate)})"
206
- end
207
- end
208
-
209
- class Applied < Expression
210
- def initialize(expression, variables={})
211
- @variables = variables
212
- @expression = expression
213
- end
214
-
215
- def to_xpath(predicate=nil)
216
- @expression.to_xpath(predicate) % @variables
217
- rescue ArgumentError # for ruby < 1.9 compat
218
- @expression.to_xpath(predicate).gsub(/%\{(\w+)\}/) do |_|
219
- @variables[$1.to_sym] or raise(ArgumentError, "expected variable #{$1} to be set")
220
- end
221
- end
222
- end
223
-
224
- class CSS < Binary
225
- def to_xpath(predicate=nil)
226
- "#{@left.to_xpath}#{@right.to_xpath}"
227
- end
6
+ def initialize(expression, *arguments)
7
+ @expression = expression
8
+ @arguments = arguments
228
9
  end
229
10
 
230
11
  def current
231
12
  self
232
13
  end
233
14
 
234
- def next_sibling(*expressions)
235
- Expression::NextSibling.new(current, expressions)
236
- end
237
-
238
- def where(expression)
239
- Expression::Where.new(current, expression)
240
- end
241
- alias_method :[], :where
242
-
243
- def one_of(*expressions)
244
- Expression::OneOf.new(current, expressions)
245
- end
246
-
247
- def equals(expression)
248
- Expression::Equality.new(current, expression)
249
- end
250
- alias_method :==, :equals
251
-
252
- def is(expression)
253
- Expression::Is.new(current, expression)
254
- end
255
-
256
- def or(expression)
257
- Expression::Or.new(current, expression)
258
- end
259
- alias_method :|, :or
260
-
261
- def and(expression)
262
- Expression::And.new(current, expression)
263
- end
264
- alias_method :&, :and
265
-
266
- def union(*expressions)
267
- Union.new(*[self, expressions].flatten)
268
- end
269
- alias_method :+, :union
270
-
271
- def inverse
272
- Expression::Inverse.new(current)
273
- end
274
- alias_method :~, :inverse
275
-
276
- def string_literal
277
- Expression::StringLiteral.new(self)
278
- end
279
-
280
- def to_xpath(predicate=nil)
281
- raise NotImplementedError, "please implement in subclass"
282
- end
283
-
284
- def to_s
285
- to_xpaths.join(' | ')
286
- end
287
-
288
- def to_xpaths
289
- [to_xpath(:exact), to_xpath(:fuzzy)].uniq
290
- end
291
-
292
- def apply(variables={})
293
- Expression::Applied.new(current, variables)
294
- end
295
-
296
- def normalize
297
- Expression::NormalizedSpace.new(current)
298
- end
299
- alias_method :n, :normalize
300
-
301
- def wrap_xpath(expression)
302
- case expression
303
- when ::String then Expression::StringLiteral.new(expression)
304
- when ::Symbol then Expression::Literal.new(expression)
305
- else expression
306
- end
15
+ def to_xpath
16
+ Renderer.render(self)
307
17
  end
18
+ alias_method :to_s, :to_xpath
308
19
  end
309
20
  end
310
-