merb_paginate 0.9.0

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 (56) hide show
  1. data/CHANGELOG.rdoc +105 -0
  2. data/LICENSE +18 -0
  3. data/README.rdoc +125 -0
  4. data/Rakefile +59 -0
  5. data/lib/merbtasks.rb +17 -0
  6. data/lib/will_paginate.rb +40 -0
  7. data/lib/will_paginate/array.rb +35 -0
  8. data/lib/will_paginate/collection.rb +145 -0
  9. data/lib/will_paginate/core_ext.rb +58 -0
  10. data/lib/will_paginate/deprecation.rb +50 -0
  11. data/lib/will_paginate/finders.rb +9 -0
  12. data/lib/will_paginate/finders/active_record.rb +192 -0
  13. data/lib/will_paginate/finders/active_record/named_scope.rb +170 -0
  14. data/lib/will_paginate/finders/active_record/named_scope_patch.rb +39 -0
  15. data/lib/will_paginate/finders/active_resource.rb +51 -0
  16. data/lib/will_paginate/finders/base.rb +112 -0
  17. data/lib/will_paginate/finders/data_mapper.rb +30 -0
  18. data/lib/will_paginate/finders/sequel.rb +21 -0
  19. data/lib/will_paginate/version.rb +9 -0
  20. data/lib/will_paginate/view_helpers.rb +42 -0
  21. data/lib/will_paginate/view_helpers/base.rb +126 -0
  22. data/lib/will_paginate/view_helpers/link_renderer.rb +130 -0
  23. data/lib/will_paginate/view_helpers/link_renderer_base.rb +83 -0
  24. data/lib/will_paginate/view_helpers/merb.rb +13 -0
  25. data/spec/collection_spec.rb +147 -0
  26. data/spec/console +8 -0
  27. data/spec/console_fixtures.rb +8 -0
  28. data/spec/database.yml +22 -0
  29. data/spec/finders/active_record_spec.rb +461 -0
  30. data/spec/finders/active_resource_spec.rb +52 -0
  31. data/spec/finders/activerecord_test_connector.rb +108 -0
  32. data/spec/finders/data_mapper_spec.rb +62 -0
  33. data/spec/finders/data_mapper_test_connector.rb +20 -0
  34. data/spec/finders/sequel_spec.rb +53 -0
  35. data/spec/finders/sequel_test_connector.rb +9 -0
  36. data/spec/finders_spec.rb +76 -0
  37. data/spec/fixtures/admin.rb +3 -0
  38. data/spec/fixtures/developer.rb +13 -0
  39. data/spec/fixtures/developers_projects.yml +13 -0
  40. data/spec/fixtures/project.rb +15 -0
  41. data/spec/fixtures/projects.yml +6 -0
  42. data/spec/fixtures/replies.yml +29 -0
  43. data/spec/fixtures/reply.rb +7 -0
  44. data/spec/fixtures/schema.rb +38 -0
  45. data/spec/fixtures/topic.rb +6 -0
  46. data/spec/fixtures/topics.yml +30 -0
  47. data/spec/fixtures/user.rb +2 -0
  48. data/spec/fixtures/users.yml +35 -0
  49. data/spec/rcov.opts +2 -0
  50. data/spec/spec.opts +2 -0
  51. data/spec/spec_helper.rb +75 -0
  52. data/spec/tasks.rake +60 -0
  53. data/spec/view_helpers/base_spec.rb +64 -0
  54. data/spec/view_helpers/link_renderer_base_spec.rb +84 -0
  55. data/spec/view_helpers/view_example_group.rb +111 -0
  56. metadata +126 -0
@@ -0,0 +1,39 @@
1
+ ## based on http://dev.rubyonrails.org/changeset/9084
2
+
3
+ ActiveRecord::Associations::AssociationProxy.class_eval do
4
+ protected
5
+ def with_scope(*args, &block)
6
+ @reflection.klass.send :with_scope, *args, &block
7
+ end
8
+ end
9
+
10
+ [ ActiveRecord::Associations::AssociationCollection,
11
+ ActiveRecord::Associations::HasManyThroughAssociation ].each do |klass|
12
+ klass.class_eval do
13
+ protected
14
+ alias :method_missing_without_scopes :method_missing_without_paginate
15
+ def method_missing_without_paginate(method, *args, &block)
16
+ if @reflection.klass.scopes.include?(method)
17
+ @reflection.klass.scopes[method].call(self, *args, &block)
18
+ else
19
+ method_missing_without_scopes(method, *args, &block)
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ # Rails 1.2.6
26
+ ActiveRecord::Associations::HasAndBelongsToManyAssociation.class_eval do
27
+ protected
28
+ def method_missing(method, *args, &block)
29
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
30
+ super
31
+ elsif @reflection.klass.scopes.include?(method)
32
+ @reflection.klass.scopes[method].call(self, *args)
33
+ else
34
+ @reflection.klass.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do
35
+ @reflection.klass.send(method, *args, &block)
36
+ end
37
+ end
38
+ end
39
+ end if ActiveRecord::Base.respond_to? :find_first
@@ -0,0 +1,51 @@
1
+ require 'will_paginate/finders/base'
2
+ require 'active_resource'
3
+
4
+ module WillPaginate::Finders
5
+ # Paginate your ActiveResource models.
6
+ #
7
+ # @posts = Post.paginate :all, :params => {
8
+ # :page => params[:page], :order => 'created_at DESC'
9
+ # }
10
+ #
11
+ module ActiveResource
12
+ include WillPaginate::Finders::Base
13
+
14
+ protected
15
+
16
+ def wp_query(options, pager, args, &block) #:nodoc:
17
+ unless args.empty? or args.first == :all
18
+ raise ArgumentError, "finder arguments other than :all are not supported for pagination (#{args.inspect} given)"
19
+ end
20
+ params = (options[:params] ||= {})
21
+ params[:page] = pager.current_page
22
+ params[:per_page] = pager.per_page
23
+
24
+ pager.replace find_every(options, &block)
25
+ end
26
+
27
+ # Takes the format that Hash.from_xml produces out of an unknown type
28
+ # (produced by WillPaginate::Collection#to_xml_with_collection_type),
29
+ # parses it into a WillPaginate::Collection,
30
+ # and forwards the result to the former +instantiate_collection+ method.
31
+ # It only does this for hashes that have a :type => "collection".
32
+ def instantiate_collection_with_collection(collection, prefix_options = {}) #:nodoc:
33
+ if collection.is_a?(Hash) && collection["type"] == "collection"
34
+ collectables = collection.values.find{ |c| c.is_a?(Hash) || c.is_a?(Array) }
35
+ collectables = [collectables].compact unless collectables.kind_of?(Array)
36
+ instantiated_collection = WillPaginate::Collection.create(collection["current_page"], collection["per_page"], collection["total_entries"]) do |pager|
37
+ pager.replace instantiate_collection_without_collection(collectables, prefix_options)
38
+ end
39
+ else
40
+ instantiate_collection_without_collection(collection, prefix_options)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ ActiveResource::Base.class_eval do
47
+ extend WillPaginate::Finders::ActiveResource
48
+ class << self
49
+ # alias_method_chain :instantiate_collection, :collection
50
+ end
51
+ end
@@ -0,0 +1,112 @@
1
+ require 'will_paginate/core_ext'
2
+
3
+ module WillPaginate
4
+ module Finders
5
+ # = Database-agnostic finder module
6
+ #
7
+ # Out of the box, will_paginate supports hooking in several ORMs to
8
+ # provide paginating finders based on their API. As of this writing, the
9
+ # supported libraries are:
10
+ #
11
+ # * ActiveRecord
12
+ # * DataMapper
13
+ # * Sequel
14
+ #
15
+ # It's easy to write your own adapter for anything that can load data with
16
+ # explicit limit and offset settings. DataMapper adapter is a nice and
17
+ # compact example of writing an adapter to bring the +paginate+ method to
18
+ # DataMapper models.
19
+ #
20
+ # == The importance of SQL's <tt>ORDER BY</tt>
21
+ #
22
+ # In most ORMs, <tt>:order</tt> parameter specifies columns for the
23
+ # <tt>ORDER BY</tt> clause in SQL. It is important to have it, since
24
+ # pagination only makes sense with ordered sets. Without the order clause,
25
+ # databases aren't required to do consistent ordering when performing
26
+ # <tt>SELECT</tt> queries.
27
+ #
28
+ # Ordering by a field for which many records share the same value (e.g.
29
+ # "status") can still result in incorrect ordering with some databases (MS
30
+ # SQL and Postgres for instance). With these databases it's recommend that
31
+ # you order by primary key as well. That is, instead of ordering by
32
+ # "status DESC", use the alternative "status DESC, id DESC" and this will
33
+ # yield consistent results.
34
+ #
35
+ # Therefore, make sure you are doing ordering on a column that makes the
36
+ # most sense in the current context. Make that obvious to the user, also.
37
+ # For perfomance reasons you will also want to add an index to that column.
38
+ module Base
39
+ def per_page
40
+ @per_page ||= 30
41
+ end
42
+
43
+ def per_page=(limit)
44
+ @per_page = limit.to_i
45
+ end
46
+
47
+ # This is the main paginating finder.
48
+ #
49
+ # == Special parameters for paginating finders
50
+ # * <tt>:page</tt> -- REQUIRED, but defaults to 1 if false or nil
51
+ # * <tt>:per_page</tt> -- defaults to <tt>CurrentModel.per_page</tt> (which is 30 if not overridden)
52
+ # * <tt>:total_entries</tt> -- use only if you manually count total entries
53
+ # * <tt>:count</tt> -- additional options that are passed on to +count+
54
+ # * <tt>:finder</tt> -- name of the finder method to use (default: "find")
55
+ #
56
+ # All other options (+conditions+, +order+, ...) are forwarded to +find+
57
+ # and +count+ calls.
58
+ def paginate(*args, &block)
59
+ options = args.pop
60
+ page, per_page, total_entries = wp_parse_options(options)
61
+
62
+ WillPaginate::Collection.create(page, per_page, total_entries) do |pager|
63
+ query_options = options.except :page, :per_page, :total_entries
64
+ wp_query(query_options, pager, args, &block)
65
+ end
66
+ end
67
+
68
+ # Iterates through all records by loading one page at a time. This is useful
69
+ # for migrations or any other use case where you don't want to load all the
70
+ # records in memory at once.
71
+ #
72
+ # It uses +paginate+ internally; therefore it accepts all of its options.
73
+ # You can specify a starting page with <tt>:page</tt> (default is 1). Default
74
+ # <tt>:order</tt> is <tt>"id"</tt>, override if necessary.
75
+ #
76
+ # {Jamis Buck describes this}[http://weblog.jamisbuck.org/2007/4/6/faking-cursors-in-activerecord]
77
+ # and also uses a more efficient way for MySQL.
78
+ def paginated_each(options = {}, &block)
79
+ options = { :order => 'id', :page => 1 }.merge options
80
+ options[:page] = options[:page].to_i
81
+ options[:total_entries] = 0 # skip the individual count queries
82
+ total = 0
83
+
84
+ begin
85
+ collection = paginate(options)
86
+ total += collection.each(&block).size
87
+ options[:page] += 1
88
+ end until collection.size < collection.per_page
89
+
90
+ total
91
+ end
92
+
93
+ protected
94
+
95
+ def wp_parse_options(options) #:nodoc:
96
+ raise ArgumentError, 'parameter hash expected' unless Hash === options
97
+ raise ArgumentError, ':page parameter required' unless options.key? :page
98
+
99
+ if options[:count] and options[:total_entries]
100
+ raise ArgumentError, ':count and :total_entries are mutually exclusive'
101
+ end
102
+
103
+ page = options[:page] || 1
104
+ per_page = options[:per_page] || self.per_page
105
+ total = options[:total_entries]
106
+
107
+ return [page, per_page, total]
108
+ end
109
+
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,30 @@
1
+ require 'will_paginate/finders/base'
2
+ require 'dm-core'
3
+
4
+ module WillPaginate::Finders
5
+ module DataMapper
6
+ include WillPaginate::Finders::Base
7
+
8
+ protected
9
+
10
+ def wp_query(options, pager, args, &block) #:nodoc
11
+ find_options = options.except(:count).update(:offset => pager.offset, :limit => pager.per_page)
12
+
13
+ pager.replace all(find_options, &block)
14
+
15
+ unless pager.total_entries
16
+ pager.total_entries = wp_count(options)
17
+ end
18
+ end
19
+
20
+ def wp_count(options) #:nodoc
21
+ count_options = options.except(:count, :order)
22
+ # merge the hash found in :count
23
+ count_options.update options[:count] if options[:count]
24
+
25
+ count_options.empty?? count() : count(count_options)
26
+ end
27
+ end
28
+ end
29
+
30
+ DataMapper::Model.send(:include, WillPaginate::Finders::DataMapper)
@@ -0,0 +1,21 @@
1
+ require 'sequel'
2
+
3
+ existing_methods = Sequel::Dataset::Pagination.instance_methods
4
+
5
+ Sequel::Dataset::Pagination.module_eval do
6
+ # it should quack like a WillPaginate::Collection
7
+
8
+ alias :total_pages :page_count unless existing_methods.include? 'total_pages'
9
+ alias :per_page :page_size unless existing_methods.include? 'per_page'
10
+ alias :previous_page :prev_page unless existing_methods.include? 'previous_page'
11
+ alias :total_entries :pagination_record_count unless existing_methods.include? 'total_entries'
12
+
13
+ def out_of_bounds?
14
+ current_page > total_pages
15
+ end
16
+
17
+ # Current offset of the paginated collection
18
+ def offset
19
+ (current_page - 1) * per_page
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ module WillPaginate #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 2
4
+ MINOR = 5
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,42 @@
1
+ require 'will_paginate/deprecation'
2
+
3
+ module WillPaginate
4
+ # = Will Paginate view helpers
5
+ #
6
+ # The main view helper is +will_paginate+. It renders the pagination links
7
+ # for the given collection. The helper itself is lightweight and serves only
8
+ # as a wrapper around LinkRenderer instantiation; the renderer then does
9
+ # all the hard work of generating the HTML.
10
+ #
11
+ # Read more in WillPaginate::ViewHelpers::Base
12
+ module ViewHelpers
13
+ # ==== Global options for helpers
14
+ #
15
+ # Options for pagination helpers are optional and get their default values
16
+ # from the WillPaginate::ViewHelpers.pagination_options hash. You can write
17
+ # to this hash to override default options on the global level:
18
+ #
19
+ # WillPaginate::ViewHelpers.pagination_options[:previous_label] = 'Previous page'
20
+ #
21
+ # By putting this into your environment.rb you can easily translate link
22
+ # texts to previous and next pages, as well as override some other defaults
23
+ # to your liking.
24
+ def self.pagination_options() @pagination_options; end
25
+ # Overrides the default +pagination_options+
26
+ def self.pagination_options=(value) @pagination_options = value; end
27
+
28
+ self.pagination_options = {
29
+ :class => 'pagination',
30
+ :previous_label => '&laquo; Previous',
31
+ :next_label => 'Next &raquo;',
32
+ :inner_window => 4, # links around the current page
33
+ :outer_window => 1, # links around beginning and end
34
+ :separator => ' ', # single space is friendly to spiders and non-graphic browsers
35
+ :param_name => :page,
36
+ :params => nil,
37
+ :renderer => 'WillPaginate::ViewHelpers::LinkRenderer',
38
+ :page_links => true,
39
+ :container => true
40
+ }
41
+ end
42
+ end
@@ -0,0 +1,126 @@
1
+ require 'will_paginate/core_ext'
2
+ require 'will_paginate/view_helpers'
3
+
4
+ module WillPaginate
5
+ module ViewHelpers
6
+ # = The main view helpers module
7
+ #
8
+ # This is the base module which provides the +will_paginate+ view helper.
9
+ module Base
10
+ # Renders Digg/Flickr-style pagination for a WillPaginate::Collection object. Nil is
11
+ # returned if there is only one page in total; pagination links aren't needed in that case.
12
+ #
13
+ # ==== Options
14
+ # * <tt>:class</tt> -- CSS class name for the generated DIV (default: "pagination")
15
+ # * <tt>:previous_label</tt> -- default: "« Previous"
16
+ # * <tt>:next_label</tt> -- default: "Next »"
17
+ # * <tt>:inner_window</tt> -- how many links are shown around the current page (default: 4)
18
+ # * <tt>:outer_window</tt> -- how many links are around the first and the last page (default: 1)
19
+ # * <tt>:separator</tt> -- string separator for page HTML elements (default: single space)
20
+ # * <tt>:param_name</tt> -- parameter name for page number in URLs (default: <tt>:page</tt>)
21
+ # * <tt>:params</tt> -- additional parameters when generating pagination links
22
+ # (eg. <tt>:controller => "foo", :action => nil</tt>)
23
+ # * <tt>:renderer</tt> -- class name, class or instance of a link renderer (default:
24
+ # <tt>WillPaginate::LinkRenderer</tt>)
25
+ # * <tt>:page_links</tt> -- when false, only previous/next links are rendered (default: true)
26
+ # * <tt>:container</tt> -- toggles rendering of the DIV container for pagination links, set to
27
+ # false only when you are rendering your own pagination markup (default: true)
28
+ # * <tt>:id</tt> -- HTML ID for the container (default: nil). Pass +true+ to have the ID
29
+ # automatically generated from the class name of objects in collection: for example, paginating
30
+ # ArticleComment models would yield an ID of "article_comments_pagination".
31
+ #
32
+ # All options beside listed ones are passed as HTML attributes to the container
33
+ # element for pagination links (the DIV). For example:
34
+ #
35
+ # <%= will_paginate @posts, :id => 'wp_posts' %>
36
+ #
37
+ # ... will result in:
38
+ #
39
+ # <div class="pagination" id="wp_posts"> ... </div>
40
+ #
41
+ def will_paginate(collection, options = {})
42
+ # early exit if there is nothing to render
43
+ return nil unless collection.total_pages > 1
44
+
45
+ options = WillPaginate::ViewHelpers.pagination_options.merge(options)
46
+
47
+ if options[:prev_label]
48
+ WillPaginate::Deprecation::warn(":prev_label view parameter is now :previous_label; the old name has been deprecated.")
49
+ options[:previous_label] = options.delete(:prev_label)
50
+ end
51
+
52
+ # get the renderer instance
53
+ renderer = case options[:renderer]
54
+ when String
55
+ options[:renderer].constantize.new
56
+ when Class
57
+ options[:renderer].new
58
+ else
59
+ options[:renderer]
60
+ end
61
+ # render HTML for pagination
62
+ renderer.prepare collection, options, self
63
+ renderer.to_html
64
+ end
65
+
66
+ # Renders a helpful message with numbers of displayed vs. total entries.
67
+ # You can use this as a blueprint for your own, similar helpers.
68
+ #
69
+ # <%= page_entries_info @posts %>
70
+ # #-> Displaying posts 6 - 10 of 26 in total
71
+ #
72
+ # By default, the message will use the humanized class name of objects
73
+ # in collection: for instance, "project types" for ProjectType models.
74
+ # Override this to your liking with the <tt>:entry_name</tt> parameter:
75
+ #
76
+ # <%= page_entries_info @posts, :entry_name => 'item' %>
77
+ # #-> Displaying items 6 - 10 of 26 in total
78
+ #
79
+ # Entry name is entered in singular and pluralized with
80
+ # <tt>String#pluralize</tt> method from ActiveSupport. If it isn't
81
+ # loaded, specify plural with <tt>:plural_name</tt> parameter:
82
+ #
83
+ # <%= page_entries_info @posts, :entry_name => 'item', :plural_name => 'items' %>
84
+ #
85
+ # By default, this method produces HTML output. You can trigger plain
86
+ # text output by passing <tt>:html => false</tt> in options.
87
+ def page_entries_info(collection, options = {})
88
+ entry_name = options[:entry_name] || (collection.empty?? 'entry' :
89
+ collection.first.class.name.underscore.gsub('_', ' '))
90
+
91
+ plural_name = if options[:plural_name]
92
+ options[:plural_name]
93
+ elsif entry_name == 'entry'
94
+ plural_name = 'entries'
95
+ elsif entry_name.respond_to? :pluralize
96
+ plural_name = entry_name.pluralize
97
+ else
98
+ entry_name + 's'
99
+ end
100
+
101
+ unless options[:html] == false
102
+ b = '<b>'
103
+ eb = '</b>'
104
+ sp = '&nbsp;'
105
+ else
106
+ b = eb = ''
107
+ sp = ' '
108
+ end
109
+
110
+ if collection.total_pages < 2
111
+ case collection.size
112
+ when 0; "No #{plural_name} found"
113
+ when 1; "Displaying #{b}1#{eb} #{entry_name}"
114
+ else; "Displaying #{b}all #{collection.size}#{eb} #{plural_name}"
115
+ end
116
+ else
117
+ %{Displaying #{plural_name} #{b}%d#{sp}-#{sp}%d#{eb} of #{b}%d#{eb} in total} % [
118
+ collection.offset + 1,
119
+ collection.offset + collection.length,
120
+ collection.total_entries
121
+ ]
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,130 @@
1
+ require 'cgi'
2
+ require 'will_paginate/core_ext'
3
+ require 'will_paginate/view_helpers/link_renderer_base'
4
+
5
+ module WillPaginate
6
+ module ViewHelpers
7
+ # This class does the heavy lifting of actually building the pagination
8
+ # links. It is used by +will_paginate+ helper internally.
9
+ class LinkRenderer < LinkRendererBase
10
+
11
+ # * +collection+ is a WillPaginate::Collection instance or any other object
12
+ # that conforms to that API
13
+ # * +options+ are forwarded from +will_paginate+ view helper
14
+ # * +template+ is the reference to the template being rendered
15
+ def prepare(collection, options, template)
16
+ super(collection, options)
17
+ @template = template
18
+ @container_attributes = @base_url_params = nil
19
+ end
20
+
21
+ # Process it! This method returns the complete HTML string which contains
22
+ # pagination links. Feel free to subclass LinkRenderer and change this
23
+ # method as you see fit.
24
+ def to_html
25
+ html = pagination.map do |item|
26
+ item.is_a?(Fixnum) ?
27
+ page_number(item) :
28
+ send(item)
29
+ end.join(@options[:separator])
30
+
31
+ @options[:container] ? html_container(html) : html
32
+ end
33
+
34
+ # Returns the subset of +options+ this instance was initialized with that
35
+ # represent HTML attributes for the container element of pagination links.
36
+ def container_attributes
37
+ @container_attributes ||= begin
38
+ attributes = @options.except *(WillPaginate::ViewHelpers.pagination_options.keys - [:class])
39
+ # pagination of Post models will have the ID of "posts_pagination"
40
+ if @options[:container] and @options[:id] === true
41
+ attributes[:id] = @collection.first.class.name.underscore.pluralize + '_pagination'
42
+ end
43
+ attributes
44
+ end
45
+ end
46
+
47
+ protected
48
+
49
+ def page_number(page)
50
+ unless page == current_page
51
+ link(page, page, :rel => rel_value(page))
52
+ else
53
+ tag(:em, page)
54
+ end
55
+ end
56
+
57
+ def gap
58
+ '<span class="gap">&hellip;</span>'
59
+ end
60
+
61
+ def previous_page
62
+ previous_or_next_page(@collection.previous_page, @options[:previous_label], 'previous_page')
63
+ end
64
+
65
+ def next_page
66
+ previous_or_next_page(@collection.next_page, @options[:next_label], 'next_page')
67
+ end
68
+
69
+ def previous_or_next_page(page, text, classname)
70
+ if page
71
+ link(text, page, :class => classname)
72
+ else
73
+ tag(:span, text, :class => classname + ' disabled')
74
+ end
75
+ end
76
+
77
+ def html_container(html)
78
+ tag(:div, html, container_attributes)
79
+ end
80
+
81
+ # Returns URL params for +page_link_or_span+, taking the current GET params
82
+ # and <tt>:params</tt> option into account.
83
+ def url(page)
84
+ raise NotImplementedError
85
+ end
86
+
87
+ private
88
+
89
+ def link(text, target, attributes = {})
90
+ if target.is_a? Fixnum
91
+ attributes[:rel] = rel_value(target)
92
+ target = url(target)
93
+ end
94
+ attributes[:href] = target
95
+ tag(:a, text, attributes)
96
+ end
97
+
98
+ def tag(name, value, attributes = {})
99
+ string_attributes = attributes.inject('') do |attrs, pair|
100
+ unless pair.last.nil?
101
+ attrs << %( #{pair.first}="#{CGI::escapeHTML(pair.last.to_s)}")
102
+ end
103
+ attrs
104
+ end
105
+ "<#{name}#{string_attributes}>#{value}</#{name}>"
106
+ end
107
+
108
+ def rel_value(page)
109
+ case page
110
+ when @collection.previous_page; 'prev' + (page == 1 ? ' start' : '')
111
+ when @collection.next_page; 'next'
112
+ when 1; 'start'
113
+ end
114
+ end
115
+
116
+ def symbolized_update(target, other)
117
+ other.each do |key, value|
118
+ key = key.to_sym
119
+ existing = target[key]
120
+
121
+ if value.is_a?(Hash) and (existing.is_a?(Hash) or existing.nil?)
122
+ symbolized_update(existing || (target[key] = {}), value)
123
+ else
124
+ target[key] = value
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end