actionpack 1.7.0 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of actionpack might be problematic. Click here for more details.
- data/CHANGELOG +109 -0
- data/README +2 -2
- data/RUNNING_UNIT_TESTS +1 -1
- data/install.rb +8 -77
- data/lib/action_controller/assertions.rb +203 -0
- data/lib/action_controller/base.rb +15 -7
- data/lib/action_controller/benchmarking.rb +10 -4
- data/lib/action_controller/caching.rb +28 -17
- data/lib/action_controller/cgi_ext/raw_post_data_fix.rb +5 -9
- data/lib/action_controller/cgi_process.rb +5 -1
- data/lib/action_controller/cookies.rb +3 -2
- data/lib/action_controller/deprecated_assertions.rb +204 -0
- data/lib/action_controller/flash.rb +30 -36
- data/lib/action_controller/pagination.rb +4 -4
- data/lib/action_controller/request.rb +18 -2
- data/lib/action_controller/routing.rb +6 -2
- data/lib/action_controller/scaffolding.rb +1 -1
- data/lib/action_controller/templates/rescues/diagnostics.rhtml +1 -1
- data/lib/action_controller/templates/rescues/routing_error.rhtml +4 -2
- data/lib/action_controller/templates/rescues/template_error.rhtml +5 -4
- data/lib/action_controller/templates/scaffolds/list.rhtml +3 -0
- data/lib/action_controller/test_process.rb +60 -17
- data/lib/action_controller/url_rewriter.rb +3 -3
- data/lib/action_controller/vendor/html-scanner/html/document.rb +63 -0
- data/lib/action_controller/vendor/html-scanner/html/node.rb +431 -0
- data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +95 -0
- data/lib/action_controller/vendor/html-scanner/html/version.rb +11 -0
- data/lib/action_controller/verification.rb +13 -4
- data/lib/action_view/base.rb +3 -2
- data/lib/action_view/helpers/active_record_helper.rb +1 -1
- data/lib/action_view/helpers/asset_tag_helper.rb +31 -9
- data/lib/action_view/helpers/date_helper.rb +25 -22
- data/lib/action_view/helpers/form_helper.rb +6 -5
- data/lib/action_view/helpers/form_options_helper.rb +4 -4
- data/lib/action_view/helpers/javascript_helper.rb +28 -6
- data/lib/action_view/helpers/javascripts/prototype.js +340 -30
- data/lib/action_view/helpers/number_helper.rb +110 -0
- data/lib/action_view/helpers/pagination_helper.rb +1 -1
- data/lib/action_view/helpers/text_helper.rb +8 -21
- data/lib/action_view/helpers/url_helper.rb +21 -4
- data/lib/action_view/partials.rb +39 -9
- data/rakefile +29 -5
- data/test/abstract_unit.rb +1 -0
- data/test/controller/action_pack_assertions_test.rb +1 -2
- data/test/controller/active_record_assertions_test.rb +1 -1
- data/test/controller/cgi_test.rb +0 -1
- data/test/controller/cookie_test.rb +11 -1
- data/test/controller/helper_test.rb +0 -12
- data/test/controller/render_test.rb +9 -0
- data/test/controller/request_test.rb +44 -1
- data/test/controller/routing_tests.rb +4 -1
- data/test/controller/test_test.rb +62 -0
- data/test/controller/verification_test.rb +21 -0
- data/test/fixtures/test/_partial_only.rhtml +1 -0
- data/test/template/active_record_helper_test.rb +2 -2
- data/test/template/asset_tag_helper_test.rb +52 -4
- data/test/template/date_helper_test.rb +163 -32
- data/test/template/form_helper_test.rb +9 -6
- data/test/template/form_options_helper_test.rb +18 -15
- data/test/template/number_helper_test.rb +51 -0
- data/test/template/text_helper_test.rb +17 -20
- data/test/template/url_helper_test.rb +7 -1
- metadata +15 -6
- data/lib/action_controller/assertions/action_pack_assertions.rb +0 -260
- data/lib/action_controller/assertions/active_record_assertions.rb +0 -65
@@ -150,8 +150,8 @@ module ActionController
|
|
150
150
|
# Returns the total number of items in the collection to be paginated for
|
151
151
|
# the +model+ and given +conditions+. Override this method to implement a
|
152
152
|
# custom counter.
|
153
|
-
def count_collection_for_pagination(model, conditions)
|
154
|
-
model.count(conditions)
|
153
|
+
def count_collection_for_pagination(model, conditions, joins)
|
154
|
+
model.count(conditions,joins)
|
155
155
|
end
|
156
156
|
|
157
157
|
# Returns a collection of items for the given +model+ and +conditions+,
|
@@ -166,9 +166,9 @@ module ActionController
|
|
166
166
|
:find_collection_for_pagination
|
167
167
|
|
168
168
|
def paginator_and_collection_for(collection_id, options) #:nodoc:
|
169
|
-
klass =
|
169
|
+
klass = options[:class_name].constantize
|
170
170
|
page = @params[options[:parameter]]
|
171
|
-
count = count_collection_for_pagination(klass, options[:conditions])
|
171
|
+
count = count_collection_for_pagination(klass, options[:conditions], options[:join])
|
172
172
|
|
173
173
|
paginator = Paginator.new(self, count, options[:per_page], page)
|
174
174
|
|
@@ -74,8 +74,16 @@ module ActionController
|
|
74
74
|
end
|
75
75
|
|
76
76
|
def request_uri
|
77
|
-
|
78
|
-
|
77
|
+
unless env['REQUEST_URI'].nil?
|
78
|
+
(%r{^\w+\://[^/]+(/.*|$)$} =~ env['REQUEST_URI']) ? $1 : env['REQUEST_URI'] # Remove domain, which webrick puts into the request_uri.
|
79
|
+
else # REQUEST_URI is blank under IIS - get this from PATH_INFO and SCRIPT_NAME
|
80
|
+
script_filename = env["SCRIPT_NAME"].to_s.match(%r{[^/]+$})
|
81
|
+
request_uri = env["PATH_INFO"]
|
82
|
+
request_uri.sub!(/#{script_filename}\//, '') unless script_filename.nil?
|
83
|
+
request_uri += '?' + env["QUERY_STRING"] unless env["QUERY_STRING"].nil? || env["QUERY_STRING"].empty?
|
84
|
+
return request_uri
|
85
|
+
end
|
86
|
+
end
|
79
87
|
|
80
88
|
def protocol
|
81
89
|
env["HTTPS"] == "on" ? 'https://' : 'http://'
|
@@ -122,6 +130,14 @@ module ActionController
|
|
122
130
|
@path_parameters ||= {}
|
123
131
|
end
|
124
132
|
|
133
|
+
# Returns true if the request's "X-Requested-With" header contains
|
134
|
+
# "XMLHttpRequest". (The Prototype Javascript library sends this header with
|
135
|
+
# every Ajax request.)
|
136
|
+
def xml_http_request?
|
137
|
+
env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i
|
138
|
+
end
|
139
|
+
alias xhr? :xml_http_request?
|
140
|
+
|
125
141
|
#--
|
126
142
|
# Must be implemented in the concrete request
|
127
143
|
#++
|
@@ -119,7 +119,11 @@ module ActionController
|
|
119
119
|
options[:controller] = controller_class.controller_path
|
120
120
|
return nil, requirements_for(:controller) unless passes_requirements?(:controller, options[:controller])
|
121
121
|
elsif /^\*/ =~ item.to_s
|
122
|
-
|
122
|
+
if components.empty?
|
123
|
+
value = @defaults.has_key?(item) ? @defaults[item].clone : []
|
124
|
+
else
|
125
|
+
value = components.clone
|
126
|
+
end
|
123
127
|
value.collect! {|c| CGI.unescape c}
|
124
128
|
components = []
|
125
129
|
def value.to_s() self.join('/') end
|
@@ -162,7 +166,7 @@ module ActionController
|
|
162
166
|
name = name.camelize
|
163
167
|
return nil, nil unless /^[A-Z][_a-zA-Z\d]*$/ =~ name
|
164
168
|
controller_name = name + "Controller"
|
165
|
-
return mod
|
169
|
+
return eval("mod::#{controller_name}"), path[length..-1] if mod.const_available? controller_name
|
166
170
|
return nil, nil unless mod.const_available? name
|
167
171
|
[mod.const_get(name), length + 1]
|
168
172
|
end
|
@@ -99,7 +99,7 @@ module ActionController
|
|
99
99
|
|
100
100
|
module_eval <<-"end_eval", __FILE__, __LINE__
|
101
101
|
def list#{suffix}
|
102
|
-
@#{plural_name} =
|
102
|
+
@#{singular_name}_pages, @#{plural_name} = paginate :#{singular_name}, :per_page => 10
|
103
103
|
render#{suffix}_scaffold "list#{suffix}"
|
104
104
|
end
|
105
105
|
|
@@ -8,7 +8,7 @@
|
|
8
8
|
<%=h @exception.class.to_s %> in
|
9
9
|
<%=h (@request.parameters["controller"] || "<controller not set>").capitalize %>#<%=h @request.parameters["action"] || "<action not set>" %>
|
10
10
|
</h1>
|
11
|
-
<
|
11
|
+
<pre><%=h Object.const_defined?(:RAILS_ROOT) ? @exception.message.gsub(RAILS_ROOT, "") : @exception.message %></pre>
|
12
12
|
|
13
13
|
<% unless app_trace.empty? %><pre><code><%=h app_trace.join("\n") %></code></pre><% end %>
|
14
14
|
|
@@ -1,8 +1,10 @@
|
|
1
1
|
<h1>Routing Error</h1>
|
2
|
-
<p><%=h @exception.message %></p>
|
2
|
+
<p><pre><%=h @exception.message %></pre></p>
|
3
3
|
<% unless @exception.failures.empty? %><p>
|
4
4
|
<h2>Failure reasons:</h2>
|
5
|
+
<ol>
|
5
6
|
<% @exception.failures.each do |route, reason| %>
|
6
|
-
|
7
|
+
<li><code><%=h route.inspect.gsub('\\', '') %></code> failed because <%=h reason.downcase %></li>
|
7
8
|
<% end %>
|
9
|
+
</ol>
|
8
10
|
</p><% end %>
|
@@ -1,14 +1,15 @@
|
|
1
1
|
<h1>
|
2
2
|
<%=h @exception.original_exception.class.to_s %> in
|
3
|
-
<%=h @request.parameters["controller"].capitalize %>#<%=h @request.parameters["action"] %>
|
3
|
+
<%=h @request.parameters["controller"].capitalize if @request.parameters["controller"]%>#<%=h @request.parameters["action"] %>
|
4
4
|
</h1>
|
5
5
|
|
6
6
|
<p>
|
7
|
-
Showing <i><%=h @exception.file_name %></i> where line <b>#<%=h @exception.line_number %></b> raised
|
8
|
-
<
|
7
|
+
Showing <i><%=h @exception.file_name %></i> where line <b>#<%=h @exception.line_number %></b> raised:
|
8
|
+
<pre><code><%=h @exception.message %></code></pre>
|
9
9
|
</p>
|
10
10
|
|
11
|
-
<
|
11
|
+
<p>Extracted source (around line <b>#<%=h @exception.line_number %></b>):
|
12
|
+
<pre><code><%=h @exception.source_extract %></code></pre></p>
|
12
13
|
|
13
14
|
<p><%=h @exception.sub_template_message %></p>
|
14
15
|
|
@@ -19,6 +19,9 @@
|
|
19
19
|
<% end %>
|
20
20
|
</table>
|
21
21
|
|
22
|
+
<%= link_to "Previous page", { :page => instance_variable_get("@#{@scaffold_singular_name}_pages").current.previous } if instance_variable_get("@#{@scaffold_singular_name}_pages").current.previous %>
|
23
|
+
<%= link_to "Next page", { :page => instance_variable_get("@#{@scaffold_singular_name}_pages").current.next } if instance_variable_get("@#{@scaffold_singular_name}_pages").current.next %>
|
24
|
+
|
22
25
|
<br />
|
23
26
|
|
24
27
|
<%= link_to "New #{@scaffold_singular_name}", :action => "new#{@scaffold_suffix}" %>
|
@@ -1,5 +1,5 @@
|
|
1
|
-
require File.dirname(__FILE__) + '/assertions
|
2
|
-
require File.dirname(__FILE__) + '/
|
1
|
+
require File.dirname(__FILE__) + '/assertions'
|
2
|
+
require File.dirname(__FILE__) + '/deprecated_assertions'
|
3
3
|
|
4
4
|
if defined?(RAILS_ROOT)
|
5
5
|
# Temporary hack for getting functional tests in Rails running under 1.8.2
|
@@ -94,17 +94,6 @@ module ActionController #:nodoc:
|
|
94
94
|
end
|
95
95
|
|
96
96
|
class TestResponse < AbstractResponse #:nodoc:
|
97
|
-
# the class attribute ties a TestResponse to the assertions
|
98
|
-
class << self
|
99
|
-
attr_accessor :assertion_target
|
100
|
-
end
|
101
|
-
|
102
|
-
# initializer
|
103
|
-
def initialize
|
104
|
-
TestResponse.assertion_target=self# if TestResponse.assertion_target.nil?
|
105
|
-
super()
|
106
|
-
end
|
107
|
-
|
108
97
|
# the response code of the request
|
109
98
|
def response_code
|
110
99
|
headers['Status'][0,3].to_i rescue 0
|
@@ -126,10 +115,12 @@ module ActionController #:nodoc:
|
|
126
115
|
end
|
127
116
|
|
128
117
|
# was there a server-side error?
|
129
|
-
def
|
118
|
+
def error?
|
130
119
|
(500..599).include?(response_code)
|
131
120
|
end
|
132
121
|
|
122
|
+
alias_method :server_error?, :error?
|
123
|
+
|
133
124
|
# returns the redirection location or nil
|
134
125
|
def redirect_url
|
135
126
|
redirect? ? headers['location'] : nil
|
@@ -252,12 +243,15 @@ module Test
|
|
252
243
|
private
|
253
244
|
# execute the request and set/volley the response
|
254
245
|
def process(action, parameters = nil, session = nil, flash = nil)
|
246
|
+
@html_document = nil
|
255
247
|
@request.env['REQUEST_METHOD'] ||= "GET"
|
256
248
|
@request.action = action.to_s
|
257
|
-
@request.path_parameters = { :controller => @controller.class.controller_path
|
249
|
+
@request.path_parameters = { :controller => @controller.class.controller_path,
|
250
|
+
:action => action.to_s }
|
258
251
|
@request.parameters.update(parameters) unless parameters.nil?
|
259
252
|
@request.session = ActionController::TestSession.new(session) unless session.nil?
|
260
253
|
@request.session["flash"] = ActionController::Flash::FlashHash.new.update(flash) if flash
|
254
|
+
build_request_uri(action, parameters)
|
261
255
|
@controller.process(@request, @response)
|
262
256
|
end
|
263
257
|
|
@@ -279,8 +273,57 @@ module Test
|
|
279
273
|
get(@response.redirected_to.delete(:action), @response.redirected_to.stringify_keys)
|
280
274
|
end
|
281
275
|
|
282
|
-
def assigns(
|
283
|
-
|
276
|
+
def assigns(key = nil)
|
277
|
+
if key.nil?
|
278
|
+
@response.template.assigns
|
279
|
+
else
|
280
|
+
@response.template.assigns[key.to_s]
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def session
|
285
|
+
@response.session
|
286
|
+
end
|
287
|
+
|
288
|
+
def flash
|
289
|
+
@response.flash
|
290
|
+
end
|
291
|
+
|
292
|
+
def cookies
|
293
|
+
@response.cookies
|
294
|
+
end
|
295
|
+
|
296
|
+
def redirect_to_url
|
297
|
+
@response.redirect_url
|
298
|
+
end
|
299
|
+
|
300
|
+
def build_request_uri(action, parameters)
|
301
|
+
return if @request.env['REQUEST_URI']
|
302
|
+
url = ActionController::UrlRewriter.new(@request, parameters)
|
303
|
+
@request.set_REQUEST_URI(
|
304
|
+
url.rewrite(@controller.send(:rewrite_options,
|
305
|
+
(parameters||{}).update(:only_path => true, :action=>action))))
|
306
|
+
end
|
307
|
+
|
308
|
+
def html_document
|
309
|
+
require_html_scanner
|
310
|
+
@html_document ||= HTML::Document.new(@response.body)
|
311
|
+
end
|
312
|
+
|
313
|
+
def find_tag(conditions)
|
314
|
+
html_document.find(conditions)
|
315
|
+
end
|
316
|
+
|
317
|
+
def find_all_tag(conditions)
|
318
|
+
html_document.find_all(conditions)
|
319
|
+
end
|
320
|
+
|
321
|
+
def require_html_scanner
|
322
|
+
return true if defined?(HTML::Document)
|
323
|
+
require 'html/document'
|
324
|
+
rescue LoadError
|
325
|
+
$:.unshift File.dirname(__FILE__) + "/vendor/html-scanner"
|
326
|
+
require 'html/document'
|
284
327
|
end
|
285
328
|
end
|
286
329
|
end
|
@@ -2,7 +2,7 @@ module ActionController
|
|
2
2
|
# Rewrites URLs for Base.redirect_to and Base.url_for in the controller.
|
3
3
|
|
4
4
|
class UrlRewriter #:nodoc:
|
5
|
-
RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :trailing_slash]
|
5
|
+
RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :trailing_slash, :skip_relative_url_root]
|
6
6
|
def initialize(request, parameters)
|
7
7
|
@request, @parameters = request, parameters
|
8
8
|
end
|
@@ -23,7 +23,7 @@ module ActionController
|
|
23
23
|
rewritten_url << (options[:protocol] || @request.protocol) unless options[:only_path]
|
24
24
|
rewritten_url << (options[:host] || @request.host_with_port) unless options[:only_path]
|
25
25
|
|
26
|
-
rewritten_url << @request.relative_url_root.to_s
|
26
|
+
rewritten_url << @request.relative_url_root.to_s unless options[:skip_relative_url_root]
|
27
27
|
rewritten_url << path
|
28
28
|
rewritten_url << '/' if options[:trailing_slash]
|
29
29
|
rewritten_url << "##{options[:anchor]}" if options[:anchor]
|
@@ -38,7 +38,7 @@ module ActionController
|
|
38
38
|
path, extras = Routing::Routes.generate(options, @request)
|
39
39
|
|
40
40
|
if extras[:overwrite_params]
|
41
|
-
params_copy = @request.parameters.
|
41
|
+
params_copy = @request.parameters.reject { |k,v| ["controller","action"].include? k }
|
42
42
|
params_copy.update extras[:overwrite_params]
|
43
43
|
extras.delete(:overwrite_params)
|
44
44
|
extras.update(params_copy)
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'html/tokenizer'
|
2
|
+
require 'html/node'
|
3
|
+
|
4
|
+
module HTML#:nodoc:
|
5
|
+
|
6
|
+
# A top-level HTMl document. You give it a body of text, and it will parse that
|
7
|
+
# text into a tree of nodes.
|
8
|
+
class Document #:nodoc:
|
9
|
+
|
10
|
+
# The root of the parsed document.
|
11
|
+
attr_reader :root
|
12
|
+
|
13
|
+
# Create a new Document from the given text.
|
14
|
+
def initialize(text)
|
15
|
+
tokenizer = Tokenizer.new(text)
|
16
|
+
@root = Node.new(nil)
|
17
|
+
node_stack = [ @root ]
|
18
|
+
while token = tokenizer.next
|
19
|
+
node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token)
|
20
|
+
|
21
|
+
node_stack.last.children << node unless node.tag? && node.closing == :close
|
22
|
+
if node.tag? && !node.childless?
|
23
|
+
if node_stack.length > 1 && node.closing == :close
|
24
|
+
if node_stack.last.name == node.name
|
25
|
+
node_stack.pop
|
26
|
+
else
|
27
|
+
open_start = node_stack.last.position - 20
|
28
|
+
open_start = 0 if open_start < 0
|
29
|
+
close_start = node.position - 20
|
30
|
+
close_start = 0 if close_start < 0
|
31
|
+
warn <<EOF.strip
|
32
|
+
ignoring attempt to close #{node_stack.last.name} with #{node.name}
|
33
|
+
opened at byte #{node_stack.last.position}, line #{node_stack.last.line}
|
34
|
+
closed at byte #{node.position}, line #{node.line}
|
35
|
+
attributes at open: #{node_stack.last.attributes.inspect}
|
36
|
+
text around open: #{text[open_start,40].inspect}
|
37
|
+
text around close: #{text[close_start,40].inspect}
|
38
|
+
EOF
|
39
|
+
end
|
40
|
+
elsif node.closing != :close
|
41
|
+
node_stack.push node
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Search the tree for (and return) the first node that matches the given
|
48
|
+
# conditions. The conditions are interpreted differently for different node
|
49
|
+
# types, see HTML::Text#find and HTML::Tag#find.
|
50
|
+
def find(conditions)
|
51
|
+
@root.find(conditions)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Search the tree for (and return) all nodes that match the given
|
55
|
+
# conditions. The conditions are interpreted differently for different node
|
56
|
+
# types, see HTML::Text#find and HTML::Tag#find.
|
57
|
+
def find_all(conditions)
|
58
|
+
@root.find_all(conditions)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,431 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module HTML#:nodoc:
|
4
|
+
|
5
|
+
class Conditions < Hash#:nodoc:
|
6
|
+
def initialize(hash)
|
7
|
+
super()
|
8
|
+
hash = { :content => hash } unless Hash === hash
|
9
|
+
hash = keys_to_symbols(hash)
|
10
|
+
hash.each do |k,v|
|
11
|
+
case k
|
12
|
+
when :tag, :content then
|
13
|
+
# keys are valid, and require no further processing
|
14
|
+
when :attributes then
|
15
|
+
hash[k] = keys_to_strings(v)
|
16
|
+
when :parent, :child, :ancestor, :descendant
|
17
|
+
hash[k] = Conditions.new(v)
|
18
|
+
when :children
|
19
|
+
hash[k] = v = keys_to_symbols(v)
|
20
|
+
v.each do |k,v2|
|
21
|
+
case k
|
22
|
+
when :count, :greater_than, :less_than
|
23
|
+
# keys are valid, and require no further processing
|
24
|
+
when :only
|
25
|
+
v[k] = Conditions.new(v2)
|
26
|
+
else
|
27
|
+
raise "illegal key #{k.inspect} => #{v2.inspect}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
else
|
31
|
+
raise "illegal key #{k.inspect} => #{v.inspect}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
update hash
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def keys_to_strings(hash)
|
40
|
+
hash.keys.inject({}) do |h,k|
|
41
|
+
h[k.to_s] = hash[k]
|
42
|
+
h
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def keys_to_symbols(hash)
|
47
|
+
hash.keys.inject({}) do |h,k|
|
48
|
+
raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym)
|
49
|
+
h[k.to_sym] = hash[k]
|
50
|
+
h
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# The base class of all nodes, textual and otherwise, in an HTML document.
|
56
|
+
class Node#:nodoc:
|
57
|
+
# The array of children of this node. Not all nodes have children.
|
58
|
+
attr_reader :children
|
59
|
+
|
60
|
+
# The parent node of this node. All nodes have a parent, except for the
|
61
|
+
# root node.
|
62
|
+
attr_reader :parent
|
63
|
+
|
64
|
+
# The line number of the input where this node was begun
|
65
|
+
attr_reader :line
|
66
|
+
|
67
|
+
# The byte position in the input where this node was begun
|
68
|
+
attr_reader :position
|
69
|
+
|
70
|
+
# Create a new node as a child of the given parent.
|
71
|
+
def initialize(parent, line=0, pos=0)
|
72
|
+
@parent = parent
|
73
|
+
@children = []
|
74
|
+
@line, @position = line, pos
|
75
|
+
end
|
76
|
+
|
77
|
+
# Return a textual representation of the node.
|
78
|
+
def to_s
|
79
|
+
s = ""
|
80
|
+
@children.each { |child| s << child.to_s }
|
81
|
+
s
|
82
|
+
end
|
83
|
+
|
84
|
+
# Return false (subclasses must override this to provide specific matching
|
85
|
+
# behavior.) +conditions+ may be of any type.
|
86
|
+
def match(conditions)
|
87
|
+
false
|
88
|
+
end
|
89
|
+
|
90
|
+
# Search the children of this node for the first node for which #find
|
91
|
+
# returns non +nil+. Returns the result of the #find call that succeeded.
|
92
|
+
def find(conditions)
|
93
|
+
@children.each do |child|
|
94
|
+
node = child.find(conditions)
|
95
|
+
return node if node
|
96
|
+
end
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
# Search for all nodes that match the given conditions, and return them
|
101
|
+
# as an array.
|
102
|
+
def find_all(conditions)
|
103
|
+
matches = []
|
104
|
+
matches << self if match(conditions)
|
105
|
+
@children.each do |child|
|
106
|
+
matches.concat child.find_all(conditions)
|
107
|
+
end
|
108
|
+
matches
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns +false+. Subclasses may override this if they define a kind of
|
112
|
+
# tag.
|
113
|
+
def tag?
|
114
|
+
false
|
115
|
+
end
|
116
|
+
|
117
|
+
def validate_conditions(conditions)
|
118
|
+
Conditions === conditions ? conditions : Conditions.new(conditions)
|
119
|
+
end
|
120
|
+
|
121
|
+
class <<self
|
122
|
+
def parse(parent, line, pos, content)
|
123
|
+
if content !~ /^<\S/
|
124
|
+
Text.new(parent, line, pos, content)
|
125
|
+
else
|
126
|
+
scanner = StringScanner.new(content)
|
127
|
+
scanner.skip(/</) or raise "expected <"
|
128
|
+
closing = ( scanner.scan(/\//) ? :close : nil )
|
129
|
+
return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:]+/)
|
130
|
+
name.downcase!
|
131
|
+
|
132
|
+
unless closing
|
133
|
+
scanner.skip(/\s*/)
|
134
|
+
attributes = {}
|
135
|
+
while attr = scanner.scan(/[-\w:]+/)
|
136
|
+
value = true
|
137
|
+
if scanner.scan(/\s*=\s*/)
|
138
|
+
if delim = scanner.scan(/['"]/)
|
139
|
+
value = ""
|
140
|
+
while text = scanner.scan(/[^#{delim}\\]+|./)
|
141
|
+
case text
|
142
|
+
when "\\" then
|
143
|
+
value << text
|
144
|
+
value << scanner.getch
|
145
|
+
when delim
|
146
|
+
break
|
147
|
+
else value << text
|
148
|
+
end
|
149
|
+
end
|
150
|
+
else
|
151
|
+
value = scanner.scan(/[^\s>\/]+/)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
attributes[attr.downcase] = value
|
155
|
+
scanner.skip(/\s*/)
|
156
|
+
end
|
157
|
+
|
158
|
+
closing = ( scanner.scan(/\//) ? :self : nil )
|
159
|
+
end
|
160
|
+
|
161
|
+
scanner.scan(/\s*>/) or raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
|
162
|
+
|
163
|
+
Tag.new(parent, line, pos, name, attributes, closing)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# A node that represents text, rather than markup.
|
170
|
+
class Text < Node#:nodoc:
|
171
|
+
|
172
|
+
attr_reader :content
|
173
|
+
|
174
|
+
# Creates a new text node as a child of the given parent, with the given
|
175
|
+
# content.
|
176
|
+
def initialize(parent, line, pos, content)
|
177
|
+
super(parent, line, pos)
|
178
|
+
@content = content
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns the content of this node.
|
182
|
+
def to_s
|
183
|
+
@content
|
184
|
+
end
|
185
|
+
|
186
|
+
# Returns +self+ if this node meets the given conditions. Text nodes support
|
187
|
+
# conditions of the following kinds:
|
188
|
+
#
|
189
|
+
# * if +conditions+ is a string, it must be a substring of the node's
|
190
|
+
# content
|
191
|
+
# * if +conditions+ is a regular expression, it must match the node's
|
192
|
+
# content
|
193
|
+
# * if +conditions+ is a hash, it must contain a <tt>:content</tt> key that
|
194
|
+
# is either a string or a regexp, and which is interpreted as described
|
195
|
+
# above.
|
196
|
+
def find(conditions)
|
197
|
+
match(conditions) && self
|
198
|
+
end
|
199
|
+
|
200
|
+
# Returns non-+nil+ if this node meets the given conditions, or +nil+
|
201
|
+
# otherwise. See the discussion of #find for the valid conditions.
|
202
|
+
def match(conditions)
|
203
|
+
case conditions
|
204
|
+
when String
|
205
|
+
@content.index(conditions)
|
206
|
+
when Regexp
|
207
|
+
@content =~ conditions
|
208
|
+
when Hash
|
209
|
+
conditions = validate_conditions(conditions)
|
210
|
+
|
211
|
+
# Text nodes only have :content, :parent, :ancestor
|
212
|
+
unless (conditions.keys - [:content, :parent, :ancestor]).empty?
|
213
|
+
return false
|
214
|
+
end
|
215
|
+
|
216
|
+
match(conditions[:content])
|
217
|
+
else
|
218
|
+
nil
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# A Tag is any node that represents markup. It may be an opening tag, a
|
224
|
+
# closing tag, or a self-closing tag. It has a name, and may have a hash of
|
225
|
+
# attributes.
|
226
|
+
class Tag < Node#:nodoc:
|
227
|
+
|
228
|
+
# Either +nil+, <tt>:close</tt>, or <tt>:self</tt>
|
229
|
+
attr_reader :closing
|
230
|
+
|
231
|
+
# Either +nil+, or a hash of attributes for this node.
|
232
|
+
attr_reader :attributes
|
233
|
+
|
234
|
+
# The name of this tag.
|
235
|
+
attr_reader :name
|
236
|
+
|
237
|
+
# Create a new node as a child of the given parent, using the given content
|
238
|
+
# to describe the node. It will be parsed and the node name, attributes and
|
239
|
+
# closing status extracted.
|
240
|
+
def initialize(parent, line, pos, name, attributes, closing)
|
241
|
+
super(parent, line, pos)
|
242
|
+
@name = name
|
243
|
+
@attributes = attributes
|
244
|
+
@closing = closing
|
245
|
+
end
|
246
|
+
|
247
|
+
# A convenience for obtaining an attribute of the node. Returns +nil+ if
|
248
|
+
# the node has no attributes.
|
249
|
+
def [](attr)
|
250
|
+
@attributes ? @attributes[attr] : nil
|
251
|
+
end
|
252
|
+
|
253
|
+
# Returns non-+nil+ if this tag can contain child nodes.
|
254
|
+
def childless?
|
255
|
+
@name =~ /^(img|br|hr|link|meta|area|base|basefont|col|frame|input|isindex|param)$/o
|
256
|
+
end
|
257
|
+
|
258
|
+
# Returns a textual representation of the node
|
259
|
+
def to_s
|
260
|
+
if @closing == :close
|
261
|
+
"</#{@name}>"
|
262
|
+
else
|
263
|
+
s = "<#{@name}"
|
264
|
+
@attributes.each { |k,v| s << " #{k}='#{v.gsub(/'/,"\\\\'")}'" }
|
265
|
+
s << " /" if @closing == :self
|
266
|
+
s << ">"
|
267
|
+
@children.each { |child| s << child.to_s }
|
268
|
+
s
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# If either the node or any of its children meet the given conditions, the
|
273
|
+
# matching node is returned. Otherwise, +nil+ is returned. (See the
|
274
|
+
# description of the valid conditions in the +match+ method.)
|
275
|
+
def find(conditions)
|
276
|
+
match(conditions) && self || super
|
277
|
+
end
|
278
|
+
|
279
|
+
# Returns +true+, indicating that this node represents an HTML tag.
|
280
|
+
def tag?
|
281
|
+
true
|
282
|
+
end
|
283
|
+
|
284
|
+
# Returns +true+ if the node meets any of the given conditions. The
|
285
|
+
# +conditions+ parameter must be a hash of any of the following keys
|
286
|
+
# (all are optional):
|
287
|
+
#
|
288
|
+
# * <tt>:tag</tt>: the node name must match the corresponding value
|
289
|
+
# * <tt>:attributes</tt>: a hash. The node's values must match the
|
290
|
+
# corresponding values in the hash.
|
291
|
+
# * <tt>:parent</tt>: a hash. The node's parent must match the
|
292
|
+
# corresponding hash.
|
293
|
+
# * <tt>:child</tt>: a hash. At least one of the node's immediate children
|
294
|
+
# must meet the criteria described by the hash.
|
295
|
+
# * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
|
296
|
+
# meet the criteria described by the hash.
|
297
|
+
# * <tt>:descendant</tt>: a hash. At least one of the node's descendants
|
298
|
+
# must meet the criteria described by the hash.
|
299
|
+
# * <tt>:children</tt>: a hash, for counting children of a node. Accepts the
|
300
|
+
# keys:
|
301
|
+
# ** <tt>:count</tt>: either a number or a range which must equal (or
|
302
|
+
# include) the number of children that match.
|
303
|
+
# ** <tt>:less_than</tt>: the number of matching children must be less than
|
304
|
+
# this number.
|
305
|
+
# ** <tt>:greater_than</tt>: the number of matching children must be
|
306
|
+
# greater than this number.
|
307
|
+
# ** <tt>:only</tt>: another hash consisting of the keys to use
|
308
|
+
# to match on the children, and only matching children will be
|
309
|
+
# counted.
|
310
|
+
#
|
311
|
+
# Conditions are matched using the following algorithm:
|
312
|
+
#
|
313
|
+
# * if the condition is a string, it must be a substring of the value.
|
314
|
+
# * if the condition is a regexp, it must match the value.
|
315
|
+
# * if the condition is a number, the value must match number.to_s.
|
316
|
+
# * if the condition is +true+, the value must not be +nil+.
|
317
|
+
# * if the condition is +false+ or +nil+, the value must be +nil+.
|
318
|
+
#
|
319
|
+
# Usage:
|
320
|
+
#
|
321
|
+
# # test if the node is a "span" tag
|
322
|
+
# node.match :tag => "span"
|
323
|
+
#
|
324
|
+
# # test if the node's parent is a "div"
|
325
|
+
# node.match :parent => { :tag => "div" }
|
326
|
+
#
|
327
|
+
# # test if any of the node's ancestors are "table" tags
|
328
|
+
# node.match :ancestor => { :tag => "table" }
|
329
|
+
#
|
330
|
+
# # test if any of the node's immediate children are "em" tags
|
331
|
+
# node.match :child => { :tag => "em" }
|
332
|
+
#
|
333
|
+
# # test if any of the node's descendants are "strong" tags
|
334
|
+
# node.match :descendant => { :tag => "strong" }
|
335
|
+
#
|
336
|
+
# # test if the node has between 2 and 4 span tags as immediate children
|
337
|
+
# node.match :children => { :count => 2..4, :only => { :tag => "span" } }
|
338
|
+
#
|
339
|
+
# # get funky: test to see if the node is a "div", has a "ul" ancestor
|
340
|
+
# # and an "li" parent (with "class" = "enum"), and whether or not it has
|
341
|
+
# # a "span" descendant that contains # text matching /hello world/:
|
342
|
+
# node.match :tag => "div",
|
343
|
+
# :ancestor => { :tag => "ul" },
|
344
|
+
# :parent => { :tag => "li",
|
345
|
+
# :attributes => { :class => "enum" } },
|
346
|
+
# :descendant => { :tag => "span",
|
347
|
+
# :child => /hello world/ }
|
348
|
+
def match(conditions)
|
349
|
+
conditions = validate_conditions(conditions)
|
350
|
+
|
351
|
+
# only Text nodes have content
|
352
|
+
return false if conditions[:content]
|
353
|
+
|
354
|
+
# test the name
|
355
|
+
return false unless match_condition(@name, conditions[:tag]) if conditions[:tag]
|
356
|
+
|
357
|
+
# test attributes
|
358
|
+
(conditions[:attributes] || {}).each do |key, value|
|
359
|
+
return false unless match_condition(self[key], value)
|
360
|
+
end
|
361
|
+
|
362
|
+
# test parent
|
363
|
+
return false unless parent.match(conditions[:parent]) if conditions[:parent]
|
364
|
+
|
365
|
+
# test children
|
366
|
+
return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child]
|
367
|
+
|
368
|
+
# test ancestors
|
369
|
+
if conditions[:ancestor]
|
370
|
+
return false unless catch :found do
|
371
|
+
p = self
|
372
|
+
throw :found, true if p.match(conditions[:ancestor]) while p = p.parent
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
# test descendants
|
377
|
+
if conditions[:descendant]
|
378
|
+
return false unless children.find do |child|
|
379
|
+
# test the child
|
380
|
+
child.match(conditions[:descendant]) ||
|
381
|
+
# test the child's descendants
|
382
|
+
child.match(:descendant => conditions[:descendant])
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
# count children
|
387
|
+
if opts = conditions[:children]
|
388
|
+
matches = children
|
389
|
+
matches = matches.select { |c| c.match(opts[:only]) } if opts[:only]
|
390
|
+
opts.each do |key, value|
|
391
|
+
next if key == :only
|
392
|
+
case key
|
393
|
+
when :count
|
394
|
+
if Integer === value
|
395
|
+
return false if matches.length != value
|
396
|
+
else
|
397
|
+
return false unless value.include?(matches.length)
|
398
|
+
end
|
399
|
+
when :less_than
|
400
|
+
return false unless matches.length < value
|
401
|
+
when :greater_than
|
402
|
+
return false unless matches.length > value
|
403
|
+
else raise "unknown count condition #{key}"
|
404
|
+
end
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
true
|
409
|
+
end
|
410
|
+
|
411
|
+
private
|
412
|
+
|
413
|
+
# Match the given value to the given condition.
|
414
|
+
def match_condition(value, condition)
|
415
|
+
case condition
|
416
|
+
when String
|
417
|
+
value && value.index(condition)
|
418
|
+
when Regexp
|
419
|
+
value && value.match(condition)
|
420
|
+
when Numeric
|
421
|
+
value == condition.to_s
|
422
|
+
when true
|
423
|
+
!value.nil?
|
424
|
+
when false, nil
|
425
|
+
value.nil?
|
426
|
+
else
|
427
|
+
false
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|