xpath 0.1.4 → 1.0.0.beta1

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 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
-