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 +61 -4
- data/lib/xpath.rb +6 -57
- data/lib/xpath/dsl.rb +104 -0
- data/lib/xpath/expression.rb +8 -298
- data/lib/xpath/html.rb +101 -62
- data/lib/xpath/literal.rb +8 -0
- data/lib/xpath/renderer.rb +146 -0
- data/lib/xpath/union.rb +10 -12
- data/lib/xpath/version.rb +1 -1
- data/spec/fixtures/form.html +49 -7
- data/spec/html_spec.rb +45 -47
- data/spec/union_spec.rb +14 -13
- data/spec/xpath_spec.rb +35 -88
- metadata +78 -69
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.
|
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
|
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
|
-
|
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
|
11
|
+
extend XPath::DSL::TopLevel
|
12
|
+
include XPath::DSL::TopLevel
|
9
13
|
|
10
14
|
def self.generate
|
11
|
-
yield(
|
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
|
data/lib/xpath/expression.rb
CHANGED
@@ -1,310 +1,20 @@
|
|
1
1
|
module XPath
|
2
2
|
class Expression
|
3
|
-
|
3
|
+
attr_accessor :expression, :arguments
|
4
|
+
include XPath::DSL::ExpressionLevel
|
4
5
|
|
5
|
-
|
6
|
-
|
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
|
235
|
-
|
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
|
-
|