pb-will_paginate 2.3.12

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.
Files changed (51) hide show
  1. data/.gitignore +4 -0
  2. data/.manifest +43 -0
  3. data/CHANGELOG.rdoc +139 -0
  4. data/LICENSE +18 -0
  5. data/README.rdoc +107 -0
  6. data/Rakefile +71 -0
  7. data/VERSION +1 -0
  8. data/examples/apple-circle.gif +0 -0
  9. data/examples/index.haml +69 -0
  10. data/examples/index.html +92 -0
  11. data/examples/pagination.css +90 -0
  12. data/examples/pagination.sass +91 -0
  13. data/init.rb +2 -0
  14. data/lib/will_paginate.rb +90 -0
  15. data/lib/will_paginate/array.rb +16 -0
  16. data/lib/will_paginate/collection.rb +144 -0
  17. data/lib/will_paginate/core_ext.rb +43 -0
  18. data/lib/will_paginate/finder.rb +264 -0
  19. data/lib/will_paginate/i18n.rb +178 -0
  20. data/lib/will_paginate/named_scope.rb +170 -0
  21. data/lib/will_paginate/named_scope_patch.rb +37 -0
  22. data/lib/will_paginate/version.rb +9 -0
  23. data/lib/will_paginate/view_helpers.rb +397 -0
  24. data/locales/en.yml +11 -0
  25. data/pb-will_paginate.gemspec +106 -0
  26. data/test/boot.rb +21 -0
  27. data/test/collection_test.rb +143 -0
  28. data/test/console +8 -0
  29. data/test/database.yml +22 -0
  30. data/test/finder_test.rb +473 -0
  31. data/test/fixtures/admin.rb +3 -0
  32. data/test/fixtures/developer.rb +14 -0
  33. data/test/fixtures/developers_projects.yml +13 -0
  34. data/test/fixtures/project.rb +15 -0
  35. data/test/fixtures/projects.yml +6 -0
  36. data/test/fixtures/replies.yml +29 -0
  37. data/test/fixtures/reply.rb +7 -0
  38. data/test/fixtures/schema.rb +38 -0
  39. data/test/fixtures/topic.rb +10 -0
  40. data/test/fixtures/topics.yml +30 -0
  41. data/test/fixtures/user.rb +2 -0
  42. data/test/fixtures/users.yml +35 -0
  43. data/test/helper.rb +37 -0
  44. data/test/i18n_test.rb +194 -0
  45. data/test/lib/activerecord_test_case.rb +43 -0
  46. data/test/lib/activerecord_test_connector.rb +75 -0
  47. data/test/lib/load_fixtures.rb +11 -0
  48. data/test/lib/view_test_process.rb +179 -0
  49. data/test/tasks.rake +59 -0
  50. data/test/view_test.rb +289 -0
  51. metadata +122 -0
@@ -0,0 +1,170 @@
1
+ module WillPaginate
2
+ # This is a feature backported from Rails 2.1 because of its usefullness not only with will_paginate,
3
+ # but in other aspects when managing complex conditions that you want to be reusable.
4
+ module NamedScope
5
+ # All subclasses of ActiveRecord::Base have two named_scopes:
6
+ # * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
7
+ # * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
8
+ #
9
+ # These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
10
+ # intermediate values (scopes) around as first-class objects is convenient.
11
+ def self.included(base)
12
+ base.class_eval do
13
+ extend ClassMethods
14
+ named_scope :scoped, lambda { |scope| scope }
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+ def scopes
20
+ read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
21
+ end
22
+
23
+ # Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
24
+ # such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
25
+ #
26
+ # class Shirt < ActiveRecord::Base
27
+ # named_scope :red, :conditions => {:color => 'red'}
28
+ # named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
29
+ # end
30
+ #
31
+ # The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
32
+ # in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
33
+ #
34
+ # Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
35
+ # constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
36
+ # <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
37
+ # as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
38
+ # <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
39
+ #
40
+ # These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
41
+ # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
42
+ # for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
43
+ #
44
+ # All scopes are available as class methods on the ActiveRecord::Base descendent upon which the scopes were defined. But they are also available to
45
+ # <tt>has_many</tt> associations. If,
46
+ #
47
+ # class Person < ActiveRecord::Base
48
+ # has_many :shirts
49
+ # end
50
+ #
51
+ # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
52
+ # only shirts.
53
+ #
54
+ # Named scopes can also be procedural.
55
+ #
56
+ # class Shirt < ActiveRecord::Base
57
+ # named_scope :colored, lambda { |color|
58
+ # { :conditions => { :color => color } }
59
+ # }
60
+ # end
61
+ #
62
+ # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
63
+ #
64
+ # Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
65
+ #
66
+ # class Shirt < ActiveRecord::Base
67
+ # named_scope :red, :conditions => {:color => 'red'} do
68
+ # def dom_id
69
+ # 'red_shirts'
70
+ # end
71
+ # end
72
+ # end
73
+ #
74
+ #
75
+ # For testing complex named scopes, you can examine the scoping options using the
76
+ # <tt>proxy_options</tt> method on the proxy itself.
77
+ #
78
+ # class Shirt < ActiveRecord::Base
79
+ # named_scope :colored, lambda { |color|
80
+ # { :conditions => { :color => color } }
81
+ # }
82
+ # end
83
+ #
84
+ # expected_options = { :conditions => { :colored => 'red' } }
85
+ # assert_equal expected_options, Shirt.colored('red').proxy_options
86
+ def named_scope(name, options = {})
87
+ name = name.to_sym
88
+ scopes[name] = lambda do |parent_scope, *args|
89
+ Scope.new(parent_scope, case options
90
+ when Hash
91
+ options
92
+ when Proc
93
+ options.call(*args)
94
+ end) { |*a| yield(*a) if block_given? }
95
+ end
96
+ (class << self; self end).instance_eval do
97
+ define_method name do |*args|
98
+ scopes[name].call(self, *args)
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ class Scope
105
+ attr_reader :proxy_scope, :proxy_options
106
+
107
+ [].methods.each do |m|
108
+ unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty\?|respond_to\?)/
109
+ delegate m, :to => :proxy_found
110
+ end
111
+ end
112
+
113
+ delegate :scopes, :with_scope, :to => :proxy_scope
114
+
115
+ def initialize(proxy_scope, options)
116
+ [options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
117
+ extend Module.new { |*args| yield(*args) } if block_given?
118
+ @proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
119
+ end
120
+
121
+ def reload
122
+ load_found; self
123
+ end
124
+
125
+ def first(*args)
126
+ if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
127
+ proxy_found.first(*args)
128
+ else
129
+ find(:first, *args)
130
+ end
131
+ end
132
+
133
+ def last(*args)
134
+ if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
135
+ proxy_found.last(*args)
136
+ else
137
+ find(:last, *args)
138
+ end
139
+ end
140
+
141
+ def empty?
142
+ @found ? @found.empty? : count.zero?
143
+ end
144
+
145
+ def respond_to?(method, include_private = false)
146
+ super || @proxy_scope.respond_to?(method, include_private)
147
+ end
148
+
149
+ protected
150
+ def proxy_found
151
+ @found || load_found
152
+ end
153
+
154
+ private
155
+ def method_missing(method, *args)
156
+ if scopes.include?(method)
157
+ scopes[method].call(self, *args)
158
+ else
159
+ with_scope :find => proxy_options do
160
+ proxy_scope.send(method, *args) { |*a| yield(*a) if block_given? }
161
+ end
162
+ end
163
+ end
164
+
165
+ def load_found
166
+ @found = find(:all)
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,37 @@
1
+ ActiveRecord::Associations::AssociationProxy.class_eval do
2
+ protected
3
+ def with_scope(*args)
4
+ @reflection.klass.send(:with_scope, *args) { |*a| yield(*a) if block_given? }
5
+ end
6
+ end
7
+
8
+ [ ActiveRecord::Associations::AssociationCollection,
9
+ ActiveRecord::Associations::HasManyThroughAssociation ].each do |klass|
10
+ klass.class_eval do
11
+ protected
12
+ alias :method_missing_without_scopes :method_missing_without_paginate
13
+ def method_missing_without_paginate(method, *args)
14
+ if @reflection.klass.scopes.include?(method)
15
+ @reflection.klass.scopes[method].call(self, *args) { |*a| yield(*a) if block_given? }
16
+ else
17
+ method_missing_without_scopes(method, *args) { |*a| yield(*a) if block_given? }
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ # Rails 1.2.6
24
+ ActiveRecord::Associations::HasAndBelongsToManyAssociation.class_eval do
25
+ protected
26
+ def method_missing(method, *args)
27
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
28
+ super
29
+ elsif @reflection.klass.scopes.include?(method)
30
+ @reflection.klass.scopes[method].call(self, *args)
31
+ else
32
+ @reflection.klass.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do
33
+ @reflection.klass.send(method, *args) { |*a| yield(*a) if block_given? }
34
+ end
35
+ end
36
+ end
37
+ end if ActiveRecord::Base.respond_to? :find_first
@@ -0,0 +1,9 @@
1
+ module WillPaginate
2
+ module VERSION
3
+ MAJOR = 2
4
+ MINOR = 3
5
+ TINY = 12
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,397 @@
1
+ require 'will_paginate/core_ext'
2
+ require 'will_paginate/i18n'
3
+
4
+ module WillPaginate
5
+ # = Will Paginate view helpers
6
+ #
7
+ # The main view helper, #will_paginate, renders
8
+ # pagination links for the given collection. The helper itself is lightweight
9
+ # and serves only as a wrapper around LinkRenderer instantiation; the
10
+ # renderer then does all the hard work of generating the HTML.
11
+ #
12
+ # == Global options for helpers
13
+ #
14
+ # Options for pagination helpers are optional and get their default values from the
15
+ # <tt>WillPaginate::ViewHelpers.pagination_options</tt> hash. You can write to this hash to
16
+ # override default options on the global level:
17
+ #
18
+ # WillPaginate::ViewHelpers.pagination_options[:renderer] = MyCustomRenderer
19
+ #
20
+ # By putting this into "config/initializers/will_paginate.rb" (or simply environment.rb in
21
+ # older versions of Rails) you can easily translate link texts to previous
22
+ # and next pages, as well as override some other defaults to your liking.
23
+ module ViewHelpers
24
+ # default options that can be overridden on the global level
25
+ @@pagination_options = {
26
+ :class => 'pagination',
27
+ :inner_window => 4, # links around the current page
28
+ :outer_window => 1, # links around beginning and end
29
+ :separator => ' ', # single space is friendly to spiders and non-graphic browsers
30
+ :param_name => :page,
31
+ :params => nil,
32
+ :renderer => 'WillPaginate::LinkRenderer',
33
+ :page_links => true,
34
+ :container => true
35
+ }
36
+ mattr_reader :pagination_options
37
+
38
+ # Renders Digg/Flickr-style pagination for a WillPaginate::Collection
39
+ # object. Nil is returned if there is only one page in total; no point in
40
+ # rendering the pagination in that case...
41
+ #
42
+ # ==== Options
43
+ # Display options:
44
+ # * <tt>:previous_label</tt> -- DEPRECATED -- see WillPaginate::LinkRenderer::I18n; (this parameter is called <tt>:prev_label</tt> in versions <b>2.3.2</b> and older!)
45
+ # * <tt>:next_label</tt> -- DEPRECATED -- see WillPaginate::LinkRenderer::I18n
46
+ # * <tt>:page_links</tt> -- when false, only previous/next links are rendered (default: true)
47
+ # * <tt>:inner_window</tt> -- how many links are shown around the current page (default: 4)
48
+ # * <tt>:outer_window</tt> -- how many links are around the first and the last page (default: 1)
49
+ # * <tt>:separator</tt> -- string separator for page HTML elements (default: single space)
50
+ #
51
+ # HTML options:
52
+ # * <tt>:class</tt> -- CSS class name for the generated DIV (default: "pagination")
53
+ # * <tt>:container</tt> -- toggles rendering of the DIV container for pagination links, set to
54
+ # false only when you are rendering your own pagination markup (default: true)
55
+ # * <tt>:id</tt> -- HTML ID for the container (default: nil). Pass +true+ to have the ID
56
+ # automatically generated from the class name of objects in collection: for example, paginating
57
+ # ArticleComment models would yield an ID of "article_comments_pagination".
58
+ #
59
+ # Advanced options:
60
+ # * <tt>:param_name</tt> -- parameter name for page number in URLs (default: <tt>:page</tt>)
61
+ # * <tt>:params</tt> -- additional parameters when generating pagination links
62
+ # (eg. <tt>:controller => "foo", :action => nil</tt>)
63
+ # * <tt>:renderer</tt> -- class name, class or instance of a link renderer (default:
64
+ # <tt>WillPaginate::LinkRenderer</tt>)
65
+ #
66
+ # All options not recognized by will_paginate will become HTML attributes on the container
67
+ # element for pagination links (the DIV). For example:
68
+ #
69
+ # <%= will_paginate @posts, :style => 'font-size: small' %>
70
+ #
71
+ # ... will result in:
72
+ #
73
+ # <div class="pagination" style="font-size: small"> ... </div>
74
+ #
75
+ # ==== Using the helper without arguments
76
+ # If the helper is called without passing in the collection object, it will
77
+ # try to read from the instance variable inferred by the controller name.
78
+ # For example, calling +will_paginate+ while the current controller is
79
+ # PostsController will result in trying to read from the <tt>@posts</tt>
80
+ # variable. Example:
81
+ #
82
+ # <%= will_paginate :id => true %>
83
+ #
84
+ # ... will result in <tt>@post</tt> collection getting paginated:
85
+ #
86
+ # <div class="pagination" id="posts_pagination"> ... </div>
87
+ #
88
+ def will_paginate(collection = nil, options = {})
89
+ options, collection = collection, nil if collection.is_a? Hash
90
+ unless collection or !controller
91
+ collection_name = "@#{controller.controller_name}"
92
+ collection = instance_variable_get(collection_name)
93
+ raise ArgumentError, "The #{collection_name} variable appears to be empty. Did you " +
94
+ "forget to pass the collection object for will_paginate?" unless collection
95
+ end
96
+ # early exit if there is nothing to render
97
+ return nil unless WillPaginate::ViewHelpers.total_pages_for_collection(collection) > 1
98
+
99
+ options = options.symbolize_keys.reverse_merge WillPaginate::ViewHelpers.pagination_options
100
+
101
+ # get the renderer instance
102
+ renderer = case options[:renderer]
103
+ when String
104
+ options[:renderer].to_s.constantize.new
105
+ when Class
106
+ options[:renderer].new
107
+ else
108
+ options[:renderer]
109
+ end
110
+ # render HTML for pagination
111
+ renderer.prepare collection, options, self
112
+ renderer.to_html
113
+ end
114
+
115
+ # Wrapper for rendering pagination links at both top and bottom of a block
116
+ # of content.
117
+ #
118
+ # <% paginated_section @posts do %>
119
+ # <ol id="posts">
120
+ # <% for post in @posts %>
121
+ # <li> ... </li>
122
+ # <% end %>
123
+ # </ol>
124
+ # <% end %>
125
+ #
126
+ # will result in:
127
+ #
128
+ # <div class="pagination"> ... </div>
129
+ # <ol id="posts">
130
+ # ...
131
+ # </ol>
132
+ # <div class="pagination"> ... </div>
133
+ #
134
+ # Arguments are passed to a <tt>will_paginate</tt> call, so the same options
135
+ # apply. Don't use the <tt>:id</tt> option; otherwise you'll finish with two
136
+ # blocks of pagination links sharing the same ID (which is invalid HTML).
137
+ def paginated_section(*args, &block)
138
+ pagination = will_paginate(*args).to_s
139
+
140
+ unless ActionView::Base.respond_to? :erb_variable
141
+ concat pagination
142
+ yield
143
+ concat pagination
144
+ else
145
+ content = pagination + capture(&block) + pagination
146
+ concat(content, block.binding)
147
+ end
148
+ end
149
+
150
+ # Renders a helpful message with numbers of displayed vs. total entries.
151
+ # You can use this as a blueprint for your own, similar helpers.
152
+ #
153
+ # <%= page_entries_info @posts %>
154
+ # #-> Displaying posts 6 - 10 of 26 in total
155
+ #
156
+ # By default, the message will use the humanized class name of objects
157
+ # in collection: for instance, "project types" for ProjectType models.
158
+ # Override this with the <tt>:entry_name</tt> parameter:
159
+ #
160
+ # <%= page_entries_info @posts, :entry_name => 'item' %>
161
+ # #-> Displaying items 6 - 10 of 26 in total
162
+ def page_entries_info(collection, options = {})
163
+ PageEntriesInfoRenderer.new(collection, options, self).page_entries_info
164
+ end
165
+
166
+ if respond_to? :safe_helper
167
+ safe_helper :will_paginate, :paginated_section, :page_entries_info
168
+ end
169
+
170
+ def self.total_pages_for_collection(collection) #:nodoc:
171
+ if collection.respond_to?('page_count') and !collection.respond_to?('total_pages')
172
+ WillPaginate::Deprecation.warn %{
173
+ You are using a paginated collection of class #{collection.class.name}
174
+ which conforms to the old API of WillPaginate::Collection by using
175
+ `page_count`, while the current method name is `total_pages`. Please
176
+ upgrade yours or 3rd-party code that provides the paginated collection}, caller
177
+ class << collection
178
+ def total_pages; page_count; end
179
+ end
180
+ end
181
+ collection.total_pages
182
+ end
183
+ end
184
+
185
+ class PageEntriesInfoRenderer
186
+ include WillPaginate::I18n
187
+ def initialize(collection, options, template)
188
+ @collection, @options, @template = collection, options, template
189
+ end
190
+ end
191
+
192
+ # This class does the heavy lifting of actually building the pagination
193
+ # links. It is used by the <tt>will_paginate</tt> helper internally.
194
+ class LinkRenderer
195
+
196
+ include WillPaginate::I18n
197
+
198
+ CURRENT_CLASS_NAME = 'current'
199
+
200
+ # DEPRECATED. See WillPaginate::I18n.
201
+ #
202
+ # The gap in page links is represented by:
203
+ #
204
+ # <span class="gap">&hellip;</span>
205
+ attr_writer :gap_marker
206
+
207
+ # * +collection+ is a WillPaginate::Collection instance or any other object
208
+ # that conforms to that API
209
+ # * +options+ are forwarded from +will_paginate+ view helper
210
+ # * +template+ is the reference to the template being rendered
211
+ def prepare(collection, options, template)
212
+ @collection = collection
213
+ @options = options
214
+ @template = template
215
+
216
+ # reset values in case we're re-using this instance
217
+ @total_pages = @param_name = @url_string = nil
218
+ end
219
+
220
+ # Process it! This method returns the complete HTML string which contains
221
+ # pagination links. Feel free to subclass LinkRenderer and change this
222
+ # method as you see fit.
223
+ def to_html
224
+ links = @options[:page_links] ? windowed_links : []
225
+ # previous/next buttons
226
+ links.unshift page_link_or_span(@collection.previous_page, 'disabled prev_page', previous_label)
227
+ links.push page_link_or_span(@collection.next_page, 'disabled next_page', next_label)
228
+
229
+ html = links.join(@options[:separator])
230
+ @options[:container] ? @template.content_tag(:div, html, html_attributes) : html
231
+ end
232
+
233
+ # Returns the subset of +options+ this instance was initialized with that
234
+ # represent HTML attributes for the container element of pagination links.
235
+ def html_attributes
236
+ return @html_attributes if @html_attributes
237
+ @html_attributes = @options.except *(WillPaginate::ViewHelpers.pagination_options.keys - [:class])
238
+ # pagination of Post models will have the ID of "posts_pagination"
239
+ if @options[:container] and @options[:id] === true
240
+ @html_attributes[:id] = @collection.first.class.name.underscore.pluralize + '_pagination'
241
+ end
242
+ @html_attributes
243
+ end
244
+
245
+ protected
246
+
247
+ # Collects link items for visible page numbers.
248
+ def windowed_links
249
+ prev = nil
250
+
251
+ visible_page_numbers.inject [] do |links, n|
252
+ # detect gaps:
253
+ links << gap_marker if prev and n > prev + 1
254
+ links << page_link_or_span(n, CURRENT_CLASS_NAME)
255
+ prev = n
256
+ links
257
+ end
258
+ end
259
+
260
+ # Calculates visible page numbers using the <tt>:inner_window</tt> and
261
+ # <tt>:outer_window</tt> options.
262
+ def visible_page_numbers
263
+ inner_window, outer_window = @options[:inner_window].to_i, @options[:outer_window].to_i
264
+ window_from = current_page - inner_window
265
+ window_to = current_page + inner_window
266
+
267
+ # adjust lower or upper limit if other is out of bounds
268
+ if window_to > total_pages
269
+ window_from -= window_to - total_pages
270
+ window_to = total_pages
271
+ end
272
+ if window_from < 1
273
+ window_to += 1 - window_from
274
+ window_from = 1
275
+ window_to = total_pages if window_to > total_pages
276
+ end
277
+
278
+ visible = (1..total_pages).to_a
279
+ left_gap = (2 + outer_window)...window_from
280
+ right_gap = (window_to + 1)...(total_pages - outer_window)
281
+ visible -= left_gap.to_a if left_gap.last - left_gap.first > 1
282
+ visible -= right_gap.to_a if right_gap.last - right_gap.first > 1
283
+
284
+ visible
285
+ end
286
+
287
+ def page_link_or_span(page, span_class, text = nil)
288
+ text ||= page.to_s
289
+
290
+ if page and page != current_page
291
+ classnames = span_class && span_class.index(' ') && span_class.split(' ', 2).last
292
+ page_link page, text, :rel => rel_value(page), :class => classnames
293
+ else
294
+ page_span page, text, :class => span_class
295
+ end
296
+ end
297
+
298
+ def page_link(page, text, attributes = {})
299
+ @template.link_to text, url_for(page), attributes
300
+ end
301
+
302
+ def page_span(page, text, attributes = {})
303
+ @template.content_tag :span, text, attributes
304
+ end
305
+
306
+ # Returns URL params for +page_link_or_span+, taking the current GET params
307
+ # and <tt>:params</tt> option into account.
308
+ def url_for(page)
309
+ page_one = page == 1
310
+ unless @url_string and !page_one
311
+ @url_params = {}
312
+ # page links should preserve GET parameters
313
+ stringified_merge @url_params, @template.params if @template.request.get?
314
+ stringified_merge @url_params, @options[:params] if @options[:params]
315
+
316
+ if complex = param_name.index(/[^\w-]/)
317
+ page_param = parse_query_parameters("#{param_name}=#{page}")
318
+
319
+ stringified_merge @url_params, page_param
320
+ else
321
+ @url_params[param_name] = page_one ? 1 : 2
322
+ end
323
+
324
+ url = @template.url_for(@url_params)
325
+ return url if page_one
326
+
327
+ if complex
328
+ @url_string = url.sub(%r!((?:\?|&amp;)#{CGI.escape param_name}=)#{page}!, "\\1\0")
329
+ return url
330
+ else
331
+ @url_string = url
332
+ @url_params[param_name] = 3
333
+ @template.url_for(@url_params).split(//).each_with_index do |char, i|
334
+ if char == '3' and url[i, 1] == '2'
335
+ @url_string[i] = "\0"
336
+ break
337
+ end
338
+ end
339
+ end
340
+ end
341
+ # finally!
342
+ @url_string.sub "\0", page.to_s
343
+ end
344
+
345
+ private
346
+
347
+ def rel_value(page)
348
+ case page
349
+ when @collection.previous_page; 'prev' + (page == 1 ? ' start' : '')
350
+ when @collection.next_page; 'next'
351
+ when 1; 'start'
352
+ end
353
+ end
354
+
355
+ def current_page
356
+ @collection.current_page
357
+ end
358
+
359
+ def total_pages
360
+ @total_pages ||= WillPaginate::ViewHelpers.total_pages_for_collection(@collection)
361
+ end
362
+
363
+ def param_name
364
+ @param_name ||= @options[:param_name].to_s
365
+ end
366
+
367
+ # Recursively merge into target hash by using stringified keys from the other one
368
+ def stringified_merge(target, other)
369
+ other.each do |key, value|
370
+ key = key.to_s # this line is what it's all about!
371
+ existing = target[key]
372
+
373
+ if value.is_a?(Hash) and (existing.is_a?(Hash) or existing.nil?)
374
+ stringified_merge(existing || (target[key] = {}), value)
375
+ else
376
+ target[key] = value
377
+ end
378
+ end
379
+ end
380
+
381
+ def parse_query_parameters(params)
382
+ if defined? Rack::Utils
383
+ # For Rails > 2.3
384
+ Rack::Utils.parse_nested_query(params)
385
+ elsif defined?(ActionController::AbstractRequest)
386
+ ActionController::AbstractRequest.parse_query_parameters(params)
387
+ elsif defined?(ActionController::UrlEncodedPairParser)
388
+ # For Rails > 2.2
389
+ ActionController::UrlEncodedPairParser.parse_query_parameters(params)
390
+ elsif defined?(CGIMethods)
391
+ CGIMethods.parse_query_parameters(params)
392
+ else
393
+ raise "unsupported ActionPack version"
394
+ end
395
+ end
396
+ end
397
+ end