j1-paginator 2020.0.1

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.
@@ -0,0 +1,104 @@
1
+ module Jekyll
2
+ module J1Paginator::Generator
3
+
4
+ # Handles the preparation of all the posts based on the current page index
5
+ class Paginator
6
+ attr_reader :page, :per_page, :posts, :total_posts, :total_pages,
7
+ :previous_page, :previous_page_path, :next_page, :next_page_path, :page_path, :page_trail,
8
+ :first_page, :first_page_path, :last_page, :last_page_path
9
+
10
+ def page_trail=(page_array)
11
+ @page_trail = page_array
12
+ end
13
+
14
+ # Initialize a new Paginator.
15
+ def initialize(config_per_page, first_index_page_url, paginated_page_url, posts, cur_page_nr, num_pages, default_indexpage, default_ext)
16
+ @page = cur_page_nr
17
+ @per_page = config_per_page.to_i
18
+ @total_pages = num_pages
19
+
20
+ if @page > @total_pages
21
+ raise RuntimeError, "page number can't be greater than total pages: #{@page} > #{@total_pages}"
22
+ end
23
+
24
+ init = (@page - 1) * @per_page
25
+ offset = (init + @per_page - 1) >= posts.size ? posts.size : (init + @per_page - 1)
26
+
27
+ # Ensure that the current page has correct extensions if needed
28
+ this_page_url = Utils.ensure_full_path(@page == 1 ? first_index_page_url : paginated_page_url,
29
+ !default_indexpage || default_indexpage.length == 0 ? 'index' : default_indexpage,
30
+ !default_ext || default_ext.length == 0 ? '.html' : default_ext)
31
+
32
+ # To support customizable pagination pages we attempt to explicitly
33
+ # append the page name to the url incase the user is using extensionless
34
+ # permalinks.
35
+ if default_indexpage && default_indexpage.length > 0
36
+ # Adjust first page url
37
+ first_index_page_url = Utils.ensure_full_path(first_index_page_url, default_indexpage, default_ext)
38
+ # Adjust the paginated pages as well
39
+ paginated_page_url = Utils.ensure_full_path(paginated_page_url, default_indexpage, default_ext)
40
+ end
41
+
42
+ @total_posts = posts.size
43
+ @posts = posts[init..offset]
44
+ @page_path = Utils.format_page_number(this_page_url, cur_page_nr, @total_pages)
45
+
46
+ @previous_page = @page != 1 ? @page - 1 : nil
47
+ @previous_page_path = @page == 1 ? nil :
48
+ @page == 2 ? Utils.format_page_number(first_index_page_url, 1, @total_pages) :
49
+ Utils.format_page_number(paginated_page_url, @previous_page, @total_pages)
50
+ @next_page = @page != @total_pages ? @page + 1 : nil
51
+ @next_page_path = @page != @total_pages ? Utils.format_page_number(paginated_page_url, @next_page, @total_pages) : nil
52
+
53
+ @first_page = 1
54
+ @first_page_path = Utils.format_page_number(first_index_page_url, 1, @total_pages)
55
+ @last_page = @total_pages
56
+ @last_page_path = Utils.format_page_number(paginated_page_url, @total_pages, @total_pages)
57
+ end
58
+
59
+ # Convert this Paginator's data to a Hash suitable for use by Liquid.
60
+ # Returns the Hash representation of this Paginator.
61
+ def to_liquid
62
+ {
63
+ 'per_page' => per_page,
64
+ 'posts' => posts,
65
+ 'total_posts' => total_posts,
66
+ 'total_pages' => total_pages,
67
+ 'page' => page,
68
+ 'page_path' => page_path,
69
+ 'previous_page' => previous_page,
70
+ 'previous_page_path' => previous_page_path,
71
+ 'next_page' => next_page,
72
+ 'next_page_path' => next_page_path,
73
+ 'first_page' => first_page,
74
+ 'first_page_path' => first_page_path,
75
+ 'last_page' => last_page,
76
+ 'last_page_path' => last_page_path,
77
+ 'page_trail' => page_trail
78
+ }
79
+ end
80
+
81
+ end # class Paginator
82
+
83
+ # Small utility class that handles individual pagination trails
84
+ # and makes them easier to work with in Liquid
85
+ class PageTrail
86
+ attr_reader :num, :path, :title
87
+
88
+ def initialize( num, path, title )
89
+ @num = num
90
+ @path = path
91
+ @title = title
92
+ end #func initialize
93
+
94
+ def to_liquid
95
+ {
96
+ 'num' => num,
97
+ 'path' => path,
98
+ 'title' => title
99
+ }
100
+ end
101
+ end #class PageTrail
102
+
103
+ end # module J1Paginator
104
+ end # module Jekyll
@@ -0,0 +1,176 @@
1
+ module Jekyll
2
+ module J1Paginator::Generator
3
+
4
+ #
5
+ # Static utility functions that are used in the code and
6
+ # don't belong in once place in particular
7
+ #
8
+ class Utils
9
+
10
+ # Static: Calculate the number of pages.
11
+ # all_posts - The Array of all Posts.
12
+ # per_page - The Integer of entries per page.
13
+ #
14
+ # Returns the Integer number of pages.
15
+ def self.calculate_number_of_pages(all_posts, per_page)
16
+ (all_posts.size.to_f / per_page.to_i).ceil
17
+ end
18
+
19
+ # Static: returns a fully formatted string with the current (:num)
20
+ # page number and maximum (:max) page count replaced if
21
+ # configured
22
+ def self.format_page_number(toFormat, cur_page_nr, total_page_count=nil)
23
+ s = toFormat.sub(':num', cur_page_nr.to_s)
24
+ if !total_page_count.nil?
25
+ s = s.sub(':max', total_page_count.to_s)
26
+ end
27
+ return s
28
+ end #function format_page_number
29
+
30
+ # Static: returns a fully formatted string with the :title variable
31
+ # and the current (:num) page number and maximum (:max) page
32
+ # count replaced
33
+ def self.format_page_title(toFormat, title, cur_page_nr=nil, total_page_count=nil)
34
+ return format_page_number(toFormat.sub(':title', title.to_s), cur_page_nr, total_page_count)
35
+ end #function format_page_title
36
+
37
+ # Static: Return a String version of the input which has a leading dot.
38
+ # If the input already has a dot in position zero, it will be
39
+ # returned unchanged.
40
+ #
41
+ # path - a String path
42
+ #
43
+ # Returns the path with a leading slash
44
+ def self.ensure_leading_dot(path)
45
+ path[0..0] == "." ? path : ".#{path}"
46
+ end
47
+
48
+ # Static: Return a String (path - a String path) version of the input
49
+ # which has a leading slash. If the input already has a forward
50
+ # slash in position zero, it will be returned unchanged.
51
+ # Returns the path with a leading slash
52
+ def self.ensure_leading_slash(path)
53
+ path[0..0] == "/" ? path : "/#{path}"
54
+ end
55
+
56
+ # Static: Return a String version of the input without a leading slash.
57
+ # path - a String path
58
+ # Returns the input without the leading slash
59
+ def self.remove_leading_slash(path)
60
+ path[0..0] == "/" ? path[1..-1] : path
61
+ end
62
+
63
+ # Static: Return a String version of the input which has a trailing slash.
64
+ # If the input already has a forward slash at the end, it will be
65
+ # returned unchanged.
66
+ #
67
+ # path - a String path
68
+ # Returns the path with a trailing slash
69
+ def self.ensure_trailing_slash(path)
70
+ path[-1] == "/" ? path : "#{path}/"
71
+ end
72
+
73
+ # Sorting routine used for ordering posts by custom fields.
74
+ # Handles Strings separately as we want a case-insenstive sorting
75
+ def self.sort_values(a, b)
76
+ if a.nil? && !b.nil?
77
+ return -1
78
+ elsif !a.nil? && b.nil?
79
+ return 1
80
+ end
81
+
82
+ if a.is_a?(String)
83
+ return a.downcase <=> b.downcase
84
+ end
85
+
86
+ if a.respond_to?('to_datetime') && b.respond_to?('to_datetime')
87
+ return a.to_datetime <=> b.to_datetime
88
+ end
89
+
90
+ # By default use the built in sorting for the data type
91
+ return a <=> b
92
+ end
93
+
94
+ # Retrieves the given sort field from the given post
95
+ # the sort_field variable can be a hierarchical value of the form
96
+ # "parent_field:child_field" repeated as many times as needed
97
+ # only the leaf child_field will be retrieved
98
+ def self.sort_get_post_data(post_data, sort_field)
99
+
100
+ # Begin by splitting up the sort_field by (;,:.)
101
+ sort_split = sort_field.split(":")
102
+ sort_value = post_data
103
+
104
+ sort_split.each do |r_key|
105
+ # Remove any erroneous whitespace and convert to lower case
106
+ key = r_key.downcase.strip
107
+ if !sort_value.has_key?(key)
108
+ return nil
109
+ end
110
+ # Work my way through the hash
111
+ sort_value = sort_value[key]
112
+ end
113
+
114
+ # If the sort value is a hash then return nil else return the value
115
+ if( sort_value.is_a?(Hash) )
116
+ return nil
117
+ else
118
+ return sort_value
119
+ end
120
+ end
121
+
122
+ # Ensures that the passed in url has a index and extension applied
123
+ def self.ensure_full_path(url, default_index, default_ext)
124
+ if( url.end_with?('/'))
125
+ return url + default_index + default_ext
126
+ elsif !url.include?('.')
127
+ return url + default_index
128
+ end
129
+ # Default
130
+ return url
131
+ end
132
+
133
+ # Constructs the plural for a key
134
+ def self.plural(config_key)
135
+ (config_key =~ /s$/) ? config_key :
136
+ (config_key.dup.sub!(/y$/, 'ies') || "#{config_key}s")
137
+ end
138
+
139
+ # Converts a string or array to a downcased, stripped array
140
+ def self.config_array(config, key, keepcase = nil)
141
+ [ config[key] ].flatten.compact.uniq.map { |c|
142
+ c.split(/[,;]\s*/).map { |v|
143
+ keepcase ? v.to_s.strip : v.to_s.downcase.strip
144
+ }
145
+ }.flatten.uniq
146
+ end
147
+
148
+ # Merges singular and plural config values into an array
149
+ def self.config_values(config, key, keepcase = nil)
150
+ singular = config_array(config, key, keepcase)
151
+ plural = config_array(config, plural(key), keepcase)
152
+ [ singular, plural ].flatten.uniq
153
+ end
154
+
155
+ def self.validate_url(template)
156
+ url = template.url
157
+ ext = template.output_ext
158
+ path = Jekyll::URL.unescape_path(url)
159
+ path = File.join(path, "index") if url.end_with?("/")
160
+ path << ext unless path.end_with?(ext)
161
+
162
+ dirname = File.dirname(path)
163
+ valid_values = [
164
+ File.join(dirname, "/"),
165
+ File.join(dirname, "index#{ext}")
166
+ ]
167
+
168
+ return url if valid_values.include?(url)
169
+ Jekyll.logger.error "Pagination Error:",
170
+ "Detected invalid url #{url.inspect} for #{template.relative_path.inspect}"
171
+ Jekyll.logger.abort_with "", "Expected #{valid_values.map(&:inspect).join(' or ')}"
172
+ end
173
+ end
174
+
175
+ end # module J1Paginator
176
+ end # module Jekyll
@@ -0,0 +1,10 @@
1
+ module Jekyll
2
+ module J1Paginator
3
+ VERSION = "2020.0.1"
4
+ # When modifying remember to issue a new tag command in git before committing, then push the new tag
5
+ # git tag -a v2.1.0 -m "Gem v2.1.0"
6
+ # git push origin --tags
7
+ # Yanking a published Gem
8
+ # gem yank j1-paginator -v VERSION
9
+ end # module J1Paginator
10
+ end # module Jekyll
@@ -0,0 +1,34 @@
1
+ require_relative '../spec_helper.rb'
2
+
3
+ module Jekyll::J1Paginator::Generator
4
+ describe "checking default config" do
5
+
6
+ it "should always contain the following keys" do
7
+ DEFAULT.must_include 'enabled'
8
+ DEFAULT.must_include 'collection'
9
+ DEFAULT.must_include 'per_page'
10
+ DEFAULT.must_include 'permalink'
11
+ DEFAULT.must_include 'title'
12
+ DEFAULT.must_include 'page_num'
13
+ DEFAULT.must_include 'sort_reverse'
14
+ DEFAULT.must_include 'sort_field'
15
+ DEFAULT.must_include 'limit'
16
+ DEFAULT.must_include 'debug'
17
+ DEFAULT.size.must_be :>=, 10
18
+ end
19
+
20
+ it "should always contain the following key defaults" do
21
+ DEFAULT['enabled'].must_equal false
22
+ DEFAULT['collection'].must_equal 'posts'
23
+ DEFAULT['per_page'].must_equal 10
24
+ DEFAULT['permalink'].must_equal '/page:num/'
25
+ DEFAULT['title'].must_equal ':title - page :num'
26
+ DEFAULT['page_num'].must_equal 1
27
+ DEFAULT['sort_reverse'].must_equal false
28
+ DEFAULT['sort_field'].must_equal 'date'
29
+ DEFAULT['limit'].must_equal 0
30
+ DEFAULT['debug'].must_equal false
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ require_relative '../spec_helper.rb'
2
+
3
+ module Jekyll::J1Paginator::Generator
4
+ describe "tesing pagination page implementation" do
5
+
6
+ it "sould always read the template file into itself" do
7
+ # DUE TO THE JEKYLL:PAGE CLASS ACCESSING FILE IO DIRECTLY
8
+ # I AM UNABLE TO MOCK OUT THE FILE OPERATIONS TO CREATE UNIT TESTS FOR THIS CLASS :/
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,197 @@
1
+ require_relative '../spec_helper.rb'
2
+
3
+ module Jekyll::J1Paginator::Generator
4
+ describe Paginator do
5
+
6
+ it "must include the necessary paginator attributes" do
7
+
8
+ # config_per_page, first_index_page_url, paginated_page_url, posts, cur_page_nr, num_pages
9
+ pager = Paginator.new(10, "index.html", "/page:num/", [], 1, 10, 'index', '.html')
10
+
11
+ # None of these accessors should throw errors, just run through them to test
12
+ val = pager.page
13
+ val = pager.per_page
14
+ val = pager.posts
15
+ val = pager.total_posts
16
+ val = pager.total_pages
17
+ val = pager.previous_page
18
+ val = pager.previous_page_path
19
+ val = pager.next_page
20
+ val = pager.next_page_path
21
+
22
+ end
23
+
24
+ it "must throw an error if the current page number is greater than the total pages" do
25
+ err = -> { pager = Paginator.new(10, "index.html", "/page:num/", [], 10, 8, 'index', '.html') }.must_raise RuntimeError
26
+
27
+ # No error should be raised below
28
+ pager = Paginator.new(10, "index.html", "/page:num/", [], 8, 10, 'index', '.html')
29
+ end
30
+
31
+ it "must trim the list of posts correctly based on the cur_page_nr and per_page" do
32
+ # Create a dummy list of posts that is easy to track
33
+ posts = [
34
+ '1','2','3','4','5',
35
+ '6','7','8','9','10',
36
+ '11','12','13','14','15',
37
+ '16','17','18','19','20',
38
+ '21','22','23','24','25',
39
+ '26','27','28','29','30',
40
+ '31','32','33','34','35'
41
+ ]
42
+
43
+ # Initialize a pager with
44
+ # 5 posts per page
45
+ # at page 2 out of 5 pages
46
+ pager = Paginator.new(5, "index.html", "/page:num/", posts, 2, 5, '', '')
47
+
48
+ pager.page.must_equal 2
49
+ pager.per_page.must_equal 5
50
+ pager.total_pages.must_equal 5
51
+
52
+ pager.total_posts.must_equal 35
53
+
54
+ pager.posts.size.must_equal 5
55
+ pager.posts[0].must_equal '6'
56
+ pager.posts[4].must_equal '10'
57
+
58
+ pager.previous_page.must_equal 1
59
+ pager.previous_page_path.must_equal 'index.html'
60
+ pager.next_page.must_equal 3
61
+ pager.next_page_path.must_equal '/page3/'
62
+ end
63
+
64
+ it "must not create a previous page if we're at first page" do
65
+ # Create a dummy list of posts that is easy to track
66
+ posts = [
67
+ '1','2','3','4','5',
68
+ '6','7','8','9','10',
69
+ '11','12','13','14','15',
70
+ '16','17','18','19','20',
71
+ '21','22','23','24','25',
72
+ '26','27','28','29','30',
73
+ '31','32','33','34','35'
74
+ ]
75
+
76
+ # Initialize a pager with
77
+ # 5 posts per page
78
+ # at page 1 out of 5 pages
79
+ pager = Paginator.new(5, "index.html", "/page:num/", posts, 1, 5, '', '')
80
+
81
+ pager.page.must_equal 1
82
+ pager.per_page.must_equal 5
83
+ pager.total_pages.must_equal 5
84
+
85
+ pager.total_posts.must_equal 35
86
+
87
+ pager.posts.size.must_equal 5
88
+ pager.posts[0].must_equal '1'
89
+ pager.posts[4].must_equal '5'
90
+
91
+ pager.previous_page.must_be_nil
92
+ pager.previous_page_path.must_be_nil
93
+ pager.next_page.must_equal 2
94
+ pager.next_page_path.must_equal '/page2/'
95
+ end
96
+
97
+ it "must not create a next page if we're at the final page" do
98
+ # Create a dummy list of posts that is easy to track
99
+ posts = [
100
+ '1','2','3','4','5',
101
+ '6','7','8','9','10',
102
+ '11','12','13','14','15',
103
+ '16','17','18','19','20',
104
+ '21','22','23','24','25',
105
+ '26','27','28','29','30',
106
+ '31','32','33','34','35'
107
+ ]
108
+
109
+ # Initialize a pager with
110
+ # 5 posts per page
111
+ # at page 5 out of 5 pages
112
+ pager = Paginator.new(5, "index.html", "/page:num/", posts, 5, 5, '', '')
113
+
114
+ pager.page.must_equal 5
115
+ pager.per_page.must_equal 5
116
+ pager.total_pages.must_equal 5
117
+
118
+ pager.total_posts.must_equal 35
119
+
120
+ pager.posts.size.must_equal 5
121
+ pager.posts[0].must_equal '21'
122
+ pager.posts[4].must_equal '25'
123
+
124
+ pager.previous_page.must_equal 4
125
+ pager.previous_page_path.must_equal '/page4/'
126
+ pager.next_page.must_be_nil
127
+ pager.next_page_path.must_be_nil
128
+ end
129
+
130
+ it "must create the explicit index page and index extension when specified" do
131
+ # Create a dummy list of posts that is easy to track
132
+ posts = [
133
+ '1','2','3','4','5',
134
+ '6','7','8','9','10',
135
+ '11','12','13','14','15',
136
+ '16','17','18','19','20',
137
+ '21','22','23','24','25',
138
+ '26','27','28','29','30',
139
+ '31','32','33','34','35'
140
+ ]
141
+
142
+ # Initialize a pager with
143
+ # 5 posts per page
144
+ # at page 2 out of 5 pages
145
+ pager = Paginator.new(5, "index.html", "/page:num/", posts, 2, 5, 'index', '.html')
146
+
147
+ pager.page.must_equal 2
148
+ pager.per_page.must_equal 5
149
+ pager.total_pages.must_equal 5
150
+
151
+ pager.total_posts.must_equal 35
152
+
153
+ pager.posts.size.must_equal 5
154
+ pager.posts[0].must_equal '6'
155
+ pager.posts[4].must_equal '10'
156
+
157
+ pager.previous_page.must_equal 1
158
+ pager.previous_page_path.must_equal 'index.html'
159
+ pager.next_page.must_equal 3
160
+ pager.next_page_path.must_equal '/page3/index.html'
161
+ end
162
+
163
+ it "must create the explicit index page and index extension when specified" do
164
+ # Create a dummy list of posts that is easy to track
165
+ posts = [
166
+ '1','2','3','4','5',
167
+ '6','7','8','9','10',
168
+ '11','12','13','14','15',
169
+ '16','17','18','19','20',
170
+ '21','22','23','24','25',
171
+ '26','27','28','29','30',
172
+ '31','32','33','34','35'
173
+ ]
174
+
175
+ # Initialize a pager with
176
+ # 5 posts per page
177
+ # at page 2 out of 5 pages
178
+ pager = Paginator.new(5, "/", "/", posts, 2, 5, 'feed:num', '.json')
179
+
180
+ pager.page.must_equal 2
181
+ pager.per_page.must_equal 5
182
+ pager.total_pages.must_equal 5
183
+
184
+ pager.total_posts.must_equal 35
185
+
186
+ pager.posts.size.must_equal 5
187
+ pager.posts[0].must_equal '6'
188
+ pager.posts[4].must_equal '10'
189
+
190
+ pager.previous_page.must_equal 1
191
+ pager.previous_page_path.must_equal '/feed1.json'
192
+ pager.next_page.must_equal 3
193
+ pager.next_page_path.must_equal '/feed3.json'
194
+ end
195
+
196
+ end
197
+ end