bridgetown-paginate 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7af8a59db674148dbfbb66a2ade0333ceb589917f22eef04533cde1cd097d65d
4
+ data.tar.gz: e75a96fc01a65161609c90923a222883dfcf500102dc9f36d7924468545fc04f
5
+ SHA512:
6
+ metadata.gz: b261fecd3cbae9ac8e8f0dfd3e5232e1fc516cd6a667cba3467e23152109d3e5a07364cfee7f694b36cd35f524532f0a01db79415649a9e999faa67c3aceb81a
7
+ data.tar.gz: 2a1d1d909a289a7f0578792b11b9eb2d1b2fc30d6cf39de4c52313f0d7eb4237fc8540ad23d994820f3d9fc03355222344bdc5c397cf1ccd3b5657b875d9606b
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,52 @@
1
+ ---
2
+ inherit_from: ../.rubocop.yml
3
+
4
+ AllCops:
5
+ Include:
6
+ - lib/**/*.rb
7
+ - spec/**/*.rb
8
+
9
+ Bridgetown/NoPutsAllowed:
10
+ Exclude:
11
+ - lib/bridgetown-paginate/pagination_model.rb
12
+ Layout/CommentIndentation:
13
+ Exclude:
14
+ - lib/bridgetown-paginate/defaults.rb
15
+ Metrics/AbcSize:
16
+ Exclude:
17
+ - lib/bridgetown-paginate/pagination_generator.rb
18
+ - lib/bridgetown-paginate/pagination_indexer.rb
19
+ - lib/bridgetown-paginate/pagination_model.rb
20
+ - lib/bridgetown-paginate/pagination_page.rb
21
+ - lib/bridgetown-paginate/paginator.rb
22
+ Metrics/BlockNesting:
23
+ Exclude:
24
+ - lib/bridgetown-paginate/pagination_model.rb
25
+ Metrics/ClassLength:
26
+ Exclude:
27
+ - lib/bridgetown-paginate/pagination_model.rb
28
+ Metrics/CyclomaticComplexity:
29
+ Exclude:
30
+ - lib/bridgetown-paginate/pagination_generator.rb
31
+ - lib/bridgetown-paginate/pagination_indexer.rb
32
+ - lib/bridgetown-paginate/pagination_model.rb
33
+ - lib/bridgetown-paginate/paginator.rb
34
+ Metrics/MethodLength:
35
+ Exclude:
36
+ - lib/bridgetown-paginate/pagination_generator.rb
37
+ - lib/bridgetown-paginate/pagination_indexer.rb
38
+ - lib/bridgetown-paginate/pagination_model.rb
39
+ - lib/bridgetown-paginate/paginator.rb
40
+ Metrics/ParameterLists:
41
+ Exclude:
42
+ - lib/bridgetown-paginate/pagination_model.rb
43
+ - lib/bridgetown-paginate/paginator.rb
44
+ Metrics/PerceivedComplexity:
45
+ Exclude:
46
+ - lib/bridgetown-paginate/pagination_generator.rb
47
+ - lib/bridgetown-paginate/pagination_indexer.rb
48
+ - lib/bridgetown-paginate/pagination_model.rb
49
+ - lib/bridgetown-paginate/paginator.rb
50
+ Style/Next:
51
+ Exclude:
52
+ - lib/bridgetown-paginate/pagination_model.rb
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../bridgetown-core/lib/bridgetown-core/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "bridgetown-paginate"
7
+ spec.version = Bridgetown::VERSION
8
+ spec.author = "Bridgetown Team"
9
+ spec.email = "maintainers@bridgetownrb.com"
10
+ spec.summary = "A Bridgetown plugin to add pagination support for posts and collection indices."
11
+ spec.homepage = "https://github.com/bridgetownrb/bridgetown-paginate"
12
+ spec.license = "MIT"
13
+
14
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r!^(test|script|spec|features)/!) }
15
+ spec.require_paths = ["lib"]
16
+
17
+ spec.add_dependency("bridgetown-core", Bridgetown::VERSION)
18
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ #################################################
4
+ # Special thanks to Sverrir Sigmundarson and the
5
+ # contributors to Jekyll::Paginate V2 for the
6
+ # basis of this gem.
7
+ # https://github.com/sverrirs/jekyll-paginate-v2
8
+ #################################################
9
+
10
+ require "bridgetown-core"
11
+ require "bridgetown-core/version"
12
+
13
+ unless ENV["BRIDGETOWN_DISABLE_PAGINATE_GEM"] == "true"
14
+ module Bridgetown
15
+ module Paginate
16
+ end
17
+ end
18
+
19
+ require "bridgetown-paginate/defaults"
20
+ require "bridgetown-paginate/utils"
21
+ require "bridgetown-paginate/pagination_indexer"
22
+ require "bridgetown-paginate/paginator"
23
+ require "bridgetown-paginate/pagination_page"
24
+ require "bridgetown-paginate/pagination_model"
25
+ require "bridgetown-paginate/pagination_generator"
26
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Paginate
5
+ module Generator
6
+ # The default configuration for the Paginator
7
+ DEFAULT = {
8
+ "enabled" => false,
9
+ "collection" => "posts",
10
+ "offset" => 0, # Supports skipping x number of posts from the
11
+ # beginning of the post list
12
+ "per_page" => 10,
13
+ "permalink" => "/page/:num/", # Supports :num as customizable elements
14
+ "title" => ":title (Page :num)", # Supports :num as customizable elements
15
+ "page_num" => 1,
16
+ "sort_reverse" => false,
17
+ "sort_field" => "date",
18
+ "limit" => 0, # Limit how many content objects to paginate (default: 0, means all)
19
+ "trail" => {
20
+ "before" => 0, # Limits how many links to show before the current page
21
+ # in the pagination trail (0, means off, default: 0)
22
+ "after" => 0, # Limits how many links to show after the current page
23
+ # in the pagination trail (0 means off, default: 0)
24
+ },
25
+ "indexpage" => nil, # The default name of the index pages
26
+ "extension" => "html", # The default extension for the output pages
27
+ # (ignored if indexpage is nil)
28
+ "debug" => false, # Turns on debug output for the gem
29
+ }.freeze
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Paginate
5
+ module Generator
6
+ #
7
+ # The main entry point into the generator, called by Bridgetown
8
+ # this function extracts all the necessary information from the Bridgetown
9
+ # end and passes it into the pagination logic. Additionally it also
10
+ # contains all site specific actions that the pagination logic needs access
11
+ # to (such as how to create new pages)
12
+ #
13
+ class PaginationGenerator < Bridgetown::Generator
14
+ # This generator should be passive with regard to its execution
15
+ priority :lowest
16
+
17
+ # Generate paginated pages if necessary (Default entry point)
18
+ # site - The Site.
19
+ #
20
+ # Returns nothing.
21
+ def generate(site)
22
+ # Retrieve and merge the pagination configuration from the site yml file
23
+ default_config = Bridgetown::Utils.deep_merge_hashes(
24
+ DEFAULT,
25
+ site.config["pagination"] || {}
26
+ )
27
+
28
+ # If disabled then simply quit
29
+ unless default_config["enabled"]
30
+ Bridgetown.logger.info "Pagination:", "Disabled in site.config."
31
+ return
32
+ end
33
+
34
+ Bridgetown.logger.debug "Pagination:", "Starting"
35
+
36
+ ################ 0 ####################
37
+ # Get all pages in the site (this will be used to find the pagination
38
+ # templates)
39
+ all_pages = site.pages
40
+
41
+ # Get the default title of the site (used as backup when there is no
42
+ # title available for pagination)
43
+ site_title = site.data.dig("metadata", "title") || site.config["title"]
44
+
45
+ ################ 1 ####################
46
+ # Specify the callback function that returns the correct docs/posts
47
+ # based on the collection name
48
+ # "posts" are just another collection in Bridgetown but a specialized
49
+ # version that require timestamps
50
+ # This collection is the default and if the user doesn't specify a
51
+ # collection in their front-matter then that is the one we load
52
+ # If the collection is not found then empty array is returned
53
+ collection_by_name_lambda = lambda do |collection_name|
54
+ coll = []
55
+ if collection_name == "all"
56
+ # the 'all' collection_name is a special case and includes all
57
+ # collections in the site (except posts!!)
58
+ # this is useful when you want to list items across multiple collections
59
+ site.collections.each do |coll_name, coll_data|
60
+ next unless !coll_data.nil? && coll_name != "posts"
61
+
62
+ # Exclude all pagination pages
63
+ coll += coll_data.docs.reject do |doc|
64
+ doc.data.key?("pagination")
65
+ end
66
+ end
67
+ else
68
+ # Just the one collection requested
69
+ return [] unless site.collections.key?(collection_name)
70
+
71
+ # Exclude all pagination pages
72
+ coll = site.collections[collection_name].docs.reject do |doc|
73
+ doc.data.key?("pagination")
74
+ end
75
+ end
76
+ return coll
77
+ end
78
+
79
+ ################ 2 ####################
80
+ # Create the proc that constructs the real-life site page
81
+ # This is necessary to decouple the code from the Bridgetown site object
82
+ page_add_lambda = lambda do |newpage|
83
+ site.pages << newpage # Add the page to the site so that it is generated correctly
84
+ return newpage # Return the site to the calling code
85
+ end
86
+
87
+ ################ 2.5 ####################
88
+ # lambda that removes a page from the site pages list
89
+ page_remove_lambda = lambda do |page_to_remove|
90
+ site.pages.delete_if { |page| page == page_to_remove }
91
+ end
92
+
93
+ ################ 3 ####################
94
+ # Create a proc that will delegate logging
95
+ # Decoupling Bridgetown specific logging
96
+ logging_lambda = lambda do |message, type = "info"|
97
+ if type == "debug"
98
+ Bridgetown.logger.debug "Pagination:", message.to_s
99
+ elsif type == "error"
100
+ Bridgetown.logger.error "Pagination:", message.to_s
101
+ elsif type == "warn"
102
+ Bridgetown.logger.warn "Pagination:", message.to_s
103
+ else
104
+ Bridgetown.logger.info "Pagination:", message.to_s
105
+ end
106
+ end
107
+
108
+ ################ 4 ####################
109
+ # Now create and call the model with the real-life page creation proc and site data
110
+ model = PaginationModel.new(
111
+ logging_lambda,
112
+ page_add_lambda,
113
+ page_remove_lambda,
114
+ collection_by_name_lambda
115
+ )
116
+ count = model.run(default_config, all_pages, site_title)
117
+ Bridgetown.logger.info "Pagination:", "Complete, processed #{count} pagination page(s)"
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Paginate
5
+ module Generator
6
+ #
7
+ # Performs indexing of the posts or collection documents as well as
8
+ # filtering said collections when requested by the defined filters.
9
+ #
10
+ class PaginationIndexer
11
+ #
12
+ # Create a hash index for all documents based on a key in the
13
+ # document.data table
14
+ #
15
+ def self.index_documents_by(all_documents, index_key)
16
+ return nil if all_documents.nil?
17
+ return all_documents if index_key.nil?
18
+
19
+ index = {}
20
+ all_documents.each do |document|
21
+ next if document.data.nil?
22
+ next unless document.data.key?(index_key)
23
+ next if document.data[index_key].nil?
24
+ next if document.data[index_key].size <= 0
25
+ next if document.data[index_key].to_s.strip.empty?
26
+
27
+ # Only tags and categories come as premade arrays, locale does not,
28
+ # so convert any data elements that are strings into arrays
29
+ document_data = document.data[index_key]
30
+ document_data = document_data.split(%r!;|,|\s!) if document_data.is_a?(String)
31
+
32
+ document_data.each do |key|
33
+ key = key.to_s.downcase.strip
34
+ # If the key is a delimetered list of values
35
+ # (meaning the user didn't use an array but a string with commas)
36
+ key.split(%r!;|,!).each do |k_split|
37
+ k_split = k_split.to_s.downcase.strip # Clean whitespace and junk
38
+ index[k_split.to_s] = [] unless index.key?(k_split)
39
+ index[k_split.to_s] << document
40
+ end
41
+ end
42
+ end
43
+
44
+ index
45
+ end
46
+
47
+ #
48
+ # Creates an intersection (only returns common elements)
49
+ # between multiple arrays
50
+ #
51
+ def self.intersect_arrays(first, *rest)
52
+ return nil if first.nil?
53
+ return nil if rest.nil?
54
+
55
+ intersect = first
56
+ rest.each do |item|
57
+ return [] if item.nil?
58
+
59
+ intersect &= item
60
+ end
61
+
62
+ intersect
63
+ end
64
+
65
+ # Filters documents based on a keyed source_documents hash of indexed
66
+ # documents and performs a intersection of the two sets. Returns only
67
+ # documents that are common between all collections
68
+ def self.read_config_value_and_filter_documents(
69
+ config,
70
+ config_key,
71
+ documents,
72
+ source_documents
73
+ )
74
+ return nil if documents.nil?
75
+
76
+ # If the source is empty then simply don't do anything
77
+ return nil if source_documents.nil?
78
+
79
+ return documents if config.nil?
80
+ return documents unless config.key?(config_key)
81
+ return documents if config[config_key].nil?
82
+
83
+ # Get the filter values from the config (this is the cat/tag/locale
84
+ # values that should be filtered on)
85
+ config_value = config[config_key]
86
+
87
+ # If we're dealing with a delimitered string instead of an array then
88
+ # let's be forgiving
89
+ config_value = config_value.split(%r!;|,!) if config_value.is_a?(String)
90
+
91
+ # Now for all filter values for the config key, let's remove all items
92
+ # from the documents that aren't common for all collections that the
93
+ # user wants to filter on
94
+ config_value.each do |key|
95
+ key = key.to_s.downcase.strip
96
+ documents = PaginationIndexer.intersect_arrays(documents, source_documents[key])
97
+ end
98
+
99
+ # The fully filtered final document list
100
+ documents
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,419 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Paginate
5
+ module Generator
6
+ #
7
+ # The main model for the pagination, handles the orchestration of the
8
+ # pagination and calling all the necessary bits and bobs needed :)
9
+ #
10
+ class PaginationModel
11
+ @debug = false # is debug output enabled?
12
+ # The lambda to use for logging
13
+ @logging_lambda = nil
14
+ # The lambda used to create pages and add them to the site
15
+ @page_add_lambda = nil
16
+ # Lambda to remove a page from the site.pages collection
17
+ @page_remove_lambda = nil
18
+ # Lambda to get all documents/posts in a particular collection (by name)
19
+ @collection_by_name_lambda = nil
20
+
21
+ def initialize(
22
+ logging_lambda,
23
+ page_add_lambda,
24
+ page_remove_lambda,
25
+ collection_by_name_lambda
26
+ )
27
+ @logging_lambda = logging_lambda
28
+ @page_add_lambda = page_add_lambda
29
+ @page_remove_lambda = page_remove_lambda
30
+ @collection_by_name_lambda = collection_by_name_lambda
31
+ end
32
+
33
+ def run(default_config, site_pages, site_title)
34
+ # By default if pagination is enabled we attempt to find all index.html
35
+ # pages in the site
36
+ templates = discover_paginate_templates(site_pages)
37
+ if templates.size.to_i <= 0
38
+ @logging_lambda.call "Is enabled, but I couldn't find any pagination page." \
39
+ " Skipping pagination. Pages must have 'pagination: enabled: true'" \
40
+ " in their front-matter for pagination to work.", "warn"
41
+ return
42
+ end
43
+
44
+ # Now for each template page generate the paginator for it
45
+ templates.each do |template|
46
+ # All pages that should be paginated need to include the pagination
47
+ # config element
48
+ if template.data["pagination"].is_a?(Hash)
49
+ template_config = Bridgetown::Utils.deep_merge_hashes(
50
+ default_config,
51
+ template.data["pagination"] || {}
52
+ )
53
+
54
+ # Is debugging enabled on the page level
55
+ @debug = template_config["debug"]
56
+
57
+ _debug_print_config_info(template_config, template.path)
58
+
59
+ # Only paginate the template if it is explicitly enabled
60
+ # requiring this makes the logic simpler as I don't need to
61
+ # determine which index pages were generated automatically and
62
+ # which weren't
63
+ if template_config["enabled"]
64
+ @logging_lambda.call "found page: " + template.path, "debug" unless @debug
65
+
66
+ # Request all documents in all collections that the user has requested
67
+ all_posts = get_docs_in_collections(template_config["collection"])
68
+
69
+ # Create the necessary indexes for the posts
70
+ all_categories = PaginationIndexer.index_documents_by(all_posts, "categories")
71
+ all_tags = PaginationIndexer.index_documents_by(all_posts, "tags")
72
+ all_locales = PaginationIndexer.index_documents_by(all_posts, "locale")
73
+
74
+ # TODO: NOTE!!! This whole request for posts and indexing results
75
+ # could be cached to improve performance, leaving like this for
76
+ # now during testing
77
+
78
+ # Now construct the pagination data for this template page
79
+ paginate(
80
+ template,
81
+ template_config,
82
+ site_title,
83
+ all_posts,
84
+ all_tags,
85
+ all_categories,
86
+ all_locales
87
+ )
88
+ end
89
+ end
90
+ end
91
+
92
+ # Return the total number of templates found
93
+ templates.size.to_i
94
+ end
95
+
96
+ # Returns the combination of all documents in the collections that are
97
+ # specified
98
+ # raw_collection_names can either be a list of collections separated by a
99
+ # ',' or ' ' or a single string
100
+ def get_docs_in_collections(raw_collection_names)
101
+ collection_names = if raw_collection_names.is_a?(String)
102
+ raw_collection_names.split %r!/;|,|\s/!
103
+ else
104
+ raw_collection_names
105
+ end
106
+
107
+ docs = []
108
+ # Now for each of the collections get the docs
109
+ collection_names.each do |coll_name|
110
+ # Request all the documents for the collection in question, and join
111
+ # it with the total collection
112
+ docs += @collection_by_name_lambda.call coll_name.downcase.strip
113
+ end
114
+
115
+ # Hidden documents should not not be processed anywhere.
116
+ docs = docs.reject { |doc| doc["hidden"] }
117
+
118
+ docs
119
+ end
120
+
121
+ # rubocop:disable Layout/LineLength
122
+ def _debug_print_config_info(config, page_path)
123
+ r = 20
124
+ f = "Pagination: ".rjust(20)
125
+ # Debug print the config
126
+ if @debug
127
+ puts f + "----------------------------"
128
+ puts f + "Page: " + page_path.to_s
129
+ puts f + " Active configuration"
130
+ puts f + " Enabled: ".ljust(r) + config["enabled"].to_s
131
+ puts f + " Items per page: ".ljust(r) + config["per_page"].to_s
132
+ puts f + " Permalink: ".ljust(r) + config["permalink"].to_s
133
+ puts f + " Title: ".ljust(r) + config["title"].to_s
134
+ puts f + " Limit: ".ljust(r) + config["limit"].to_s
135
+ puts f + " Sort by: ".ljust(r) + config["sort_field"].to_s
136
+ puts f + " Sort reverse: ".ljust(r) + config["sort_reverse"].to_s
137
+
138
+ puts f + " Active Filters"
139
+ puts f + " Collection: ".ljust(r) + config["collection"].to_s
140
+ puts f + " Offset: ".ljust(r) + config["offset"].to_s
141
+ puts f + " Category: ".ljust(r) + (config["category"].nil? || config["category"] == "posts" ? "[Not set]" : config["category"].to_s)
142
+ puts f + " Tag: ".ljust(r) + (config["tag"].nil? ? "[Not set]" : config["tag"].to_s)
143
+ puts f + " Locale: ".ljust(r) + (config["locale"].nil? ? "[Not set]" : config["locale"].to_s)
144
+ end
145
+ end
146
+ # rubocop:enable Layout/LineLength
147
+
148
+ # rubocop:disable Layout/LineLength
149
+ def _debug_print_filtering_info(filter_name, before_count, after_count)
150
+ # Debug print the config
151
+ if @debug
152
+ puts "Pagination: ".rjust(20) + " Filtering by: " + filter_name.to_s.ljust(9) + " " + before_count.to_s.rjust(3) + " => " + after_count.to_s
153
+ end
154
+ end
155
+ # rubocop:enable Layout/LineLength
156
+
157
+ #
158
+ # Rolls through all the pages passed in and finds all pages that have
159
+ # pagination enabled on them.
160
+ # These pages will be used as templates
161
+ #
162
+ # site_pages - All pages in the site
163
+ #
164
+ def discover_paginate_templates(site_pages)
165
+ site_pages.select do |page|
166
+ page.data["pagination"].is_a?(Hash) && page.data["pagination"]["enabled"]
167
+ end
168
+ end
169
+
170
+ # Paginates the blog's posts. Renders the index.html file into paginated
171
+ # directories, e.g.: page2/index.html, page3/index.html, etc and adds more
172
+ # site-wide data.
173
+ #
174
+ # site - The Site.
175
+ # template - The index.html Page that requires pagination.
176
+ # config - The configuration settings that should be used
177
+ #
178
+ def paginate(
179
+ template,
180
+ config,
181
+ site_title,
182
+ all_posts,
183
+ all_tags,
184
+ all_categories,
185
+ all_locales
186
+ )
187
+ # By default paginate on all posts in the site
188
+ using_posts = all_posts
189
+
190
+ # Now start filtering out any posts that the user doesn't want included
191
+ # in the pagination
192
+ before = using_posts.size.to_i
193
+ using_posts = PaginationIndexer.read_config_value_and_filter_documents(
194
+ config,
195
+ "category",
196
+ using_posts,
197
+ all_categories
198
+ )
199
+ _debug_print_filtering_info("Category", before, using_posts.size.to_i)
200
+ before = using_posts.size.to_i
201
+ using_posts = PaginationIndexer.read_config_value_and_filter_documents(
202
+ config,
203
+ "tag",
204
+ using_posts,
205
+ all_tags
206
+ )
207
+ _debug_print_filtering_info("Tag", before, using_posts.size.to_i)
208
+ before = using_posts.size.to_i
209
+ using_posts = PaginationIndexer.read_config_value_and_filter_documents(
210
+ config,
211
+ "locale",
212
+ using_posts,
213
+ all_locales
214
+ )
215
+ _debug_print_filtering_info("Locale", before, using_posts.size.to_i)
216
+
217
+ # Apply sorting to the posts if configured, any field for the post is
218
+ # available for sorting
219
+ if config["sort_field"]
220
+ sort_field = config["sort_field"].to_s
221
+
222
+ # There is an issue in Bridgetown related to lazy initialized member
223
+ # variables that causes iterators to
224
+ # break when accessing an uninitialized value during iteration. This
225
+ # happens for document.rb when the <=> comparison function
226
+ # is called (as this function calls the 'date' field which for drafts
227
+ # are not initialized.)
228
+ # So to unblock this common issue for the date field I simply iterate
229
+ # once over every document and initialize the .date field explicitly
230
+ if @debug
231
+ puts "Pagination: ".rjust(20) + "Rolling through the date fields for all documents"
232
+ end
233
+ using_posts.each do |u_post|
234
+ next unless u_post.respond_to?("date")
235
+
236
+ tmp_date = u_post.date
237
+ if !tmp_date || tmp_date.nil?
238
+ if @debug
239
+ puts "Pagination: ".rjust(20) +
240
+ "Explicitly assigning date for doc: #{u_post.data["title"]} | #{u_post.path}"
241
+ end
242
+ u_post.date = File.mtime(u_post.path)
243
+ end
244
+ end
245
+
246
+ using_posts.sort! do |a, b|
247
+ Utils.sort_values(
248
+ Utils.sort_get_post_data(a.data, sort_field),
249
+ Utils.sort_get_post_data(b.data, sort_field)
250
+ )
251
+ end
252
+
253
+ # Remove the first x entries
254
+ offset_post_count = [0, config["offset"].to_i].max
255
+ using_posts.pop(offset_post_count)
256
+
257
+ using_posts.reverse! if config["sort_reverse"]
258
+ end
259
+
260
+ # Calculate the max number of pagination-pages based on the configured per page value
261
+ total_pages = Utils.calculate_number_of_pages(using_posts, config["per_page"])
262
+
263
+ # If a upper limit is set on the number of total pagination pages then impose that now
264
+ if config["limit"].to_i.positive? && config["limit"].to_i < total_pages
265
+ total_pages = config["limit"].to_i
266
+ end
267
+
268
+ #### BEFORE STARTING REMOVE THE TEMPLATE PAGE FROM THE SITE LIST!
269
+ @page_remove_lambda.call template
270
+
271
+ # list of all newly created pages
272
+ newpages = []
273
+
274
+ # Consider the default index page name and extension
275
+ index_page_name = if config["indexpage"].nil?
276
+ ""
277
+ else
278
+ config["indexpage"].split(".")[0]
279
+ end
280
+ index_page_ext = if config["extension"].nil?
281
+ ""
282
+ else
283
+ Utils.ensure_leading_dot(config["extension"])
284
+ end
285
+ index_page_with_ext = index_page_name + index_page_ext
286
+
287
+ # In case there are no (visible) posts, generate the index file anyway
288
+ total_pages = 1 if total_pages.zero?
289
+
290
+ # Now for each pagination page create it and configure the ranges for
291
+ # the collection
292
+ # The .pager member is a built in thing in Bridgetown and references the
293
+ # paginator implementation
294
+ # rubocop:disable Metrics/BlockLength
295
+ (1..total_pages).each do |cur_page_nr|
296
+ # 1. Create the in-memory page
297
+ # External Proc call to create the actual page for us (this is
298
+ # passed in when the pagination is run)
299
+ newpage = PaginationPage.new template, cur_page_nr, total_pages, index_page_with_ext
300
+
301
+ # 2. Create the url for the in-memory page (calc permalink etc),
302
+ # construct the title, set all page.data values needed
303
+ paginated_page_url = config["permalink"]
304
+ first_index_page_url = if template.data["permalink"]
305
+ Utils.ensure_trailing_slash(
306
+ template.data["permalink"]
307
+ )
308
+ else
309
+ Utils.ensure_trailing_slash(template.dir)
310
+ end
311
+ paginated_page_url = File.join(first_index_page_url, paginated_page_url)
312
+
313
+ # 3. Create the pager logic for this page, pass in the prev and next
314
+ # page numbers, assign pager to in-memory page
315
+ newpage.pager = Paginator.new(
316
+ config["per_page"],
317
+ first_index_page_url,
318
+ paginated_page_url,
319
+ using_posts,
320
+ cur_page_nr,
321
+ total_pages,
322
+ index_page_name,
323
+ index_page_ext
324
+ )
325
+
326
+ # Create the url for the new page, make sure we prepend any permalinks
327
+ # that are defined in the template page before
328
+ if newpage.pager.page_path.end_with? "/"
329
+ newpage.set_url(File.join(newpage.pager.page_path, index_page_with_ext))
330
+ elsif newpage.pager.page_path.end_with? index_page_ext
331
+ # Support for direct .html files
332
+ newpage.set_url(newpage.pager.page_path)
333
+ else
334
+ # Support for extensionless permalinks
335
+ newpage.set_url(newpage.pager.page_path + index_page_ext)
336
+ end
337
+
338
+ newpage.data["permalink"] = newpage.pager.page_path if template.data["permalink"]
339
+
340
+ # Transfer the title across to the new page
341
+ tmp_title = if !template.data["title"]
342
+ site_title
343
+ else
344
+ template.data["title"]
345
+ end
346
+
347
+ # If the user specified a title suffix to be added then let's add that
348
+ # to all the pages except the first
349
+ if cur_page_nr > 1 && config.key?("title")
350
+ newtitle = Utils.format_page_title(
351
+ config["title"],
352
+ tmp_title,
353
+ cur_page_nr,
354
+ total_pages
355
+ )
356
+ newpage.data["title"] = newtitle.to_s
357
+ else
358
+ newpage.data["title"] = tmp_title
359
+ end
360
+
361
+ # Signals that this page is automatically generated by the pagination logic
362
+ # (we don't do this for the first page as it is there to mask the one we removed)
363
+ newpage.data["autogen"] = "bridgetown-paginate" if cur_page_nr > 1
364
+
365
+ # Add the page to the site
366
+ @page_add_lambda.call newpage
367
+
368
+ # Store the page in an internal list for later referencing if we need
369
+ # to generate a pagination number path later on
370
+ newpages << newpage
371
+ end
372
+ # rubocop:enable Metrics/BlockLength
373
+
374
+ # Now generate the pagination number path, e.g. so that the users can
375
+ # have a prev 1 2 3 4 5 next structure on their page
376
+ # simplest is to include all of the links to the pages preceeding the
377
+ # current one (e.g for page 1 you get the list 2, 3, 4.... and for
378
+ # page 2 you get the list 3,4,5...)
379
+ if config["trail"] && !config["trail"].nil? && newpages.size.to_i.positive?
380
+ trail_before = [config["trail"]["before"].to_i, 0].max
381
+ trail_after = [config["trail"]["after"].to_i, 0].max
382
+ trail_length = trail_before + trail_after + 1
383
+
384
+ if trail_before.positive? || trail_after.positive?
385
+ newpages.select do |npage|
386
+ # Selecting the beginning of the trail
387
+ idx_start = [npage.pager.page - trail_before - 1, 0].max
388
+ # Selecting the end of the trail
389
+ idx_end = [idx_start + trail_length, newpages.size.to_i].min
390
+
391
+ # Always attempt to maintain the max total of <trail_length> pages
392
+ # in the trail (it will look better if the trail doesn't shrink)
393
+ if idx_end - idx_start < trail_length
394
+ # Attempt to pad the beginning if we have enough pages
395
+ # Never go beyond the zero index
396
+ idx_start = [
397
+ idx_start - (trail_length - (idx_end - idx_start)),
398
+ 0,
399
+ ].max
400
+ end
401
+
402
+ # Convert the newpages array into a two dimensional array that has
403
+ # [index, page_url] as items
404
+ npage.pager.page_trail = newpages[idx_start...idx_end] \
405
+ .each_with_index.map do |ipage, idx|
406
+ PageTrail.new(
407
+ idx_start + idx + 1,
408
+ ipage.pager.page_path,
409
+ ipage.data["title"]
410
+ )
411
+ end
412
+ end
413
+ end
414
+ end
415
+ end
416
+ end
417
+ end
418
+ end
419
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Paginate
5
+ module Generator
6
+ #
7
+ # This page handles the creation of the fake pagination pages based on the
8
+ # original page configuration.
9
+ # The code does the same things as the default Bridgetown page.rb code but
10
+ # just forces the code to look into the template instead of the (currently
11
+ # non-existing) pagination page. This page exists purely in memory and is
12
+ # not read from disk
13
+ #
14
+ class PaginationPage < Bridgetown::Page
15
+ def initialize(page_to_copy, cur_page_nr, total_pages, index_pageandext)
16
+ @site = page_to_copy.site
17
+ @base = ""
18
+ @url = ""
19
+ @name = index_pageandext.nil? ? "index.html" : index_pageandext
20
+ @path = page_to_copy.path
21
+
22
+ # Creates the basename and ext member values
23
+ process(@name)
24
+
25
+ # Only need to copy the data part of the page as it already contains the
26
+ # layout information
27
+ self.data = Bridgetown::Utils.deep_merge_hashes page_to_copy.data, {}
28
+ if !page_to_copy.data["autopage"]
29
+ self.content = page_to_copy.content
30
+ elsif page_to_copy.data["autopage"].key?("display_name")
31
+ # If the page is an auto page then migrate the necessary autopage info
32
+ # across into the new pagination page (so that users can get the
33
+ # correct keys etc)
34
+ data["autopages"] = Bridgetown::Utils.deep_merge_hashes(
35
+ page_to_copy.data["autopage"], {}
36
+ )
37
+ end
38
+
39
+ # Store the current page and total page numbers in the pagination_info construct
40
+ data["pagination_info"] = { "curr_page" => cur_page_nr, "total_pages" => total_pages }
41
+
42
+ # Perform some validation that is also performed in Bridgetown::Page
43
+ validate_data! page_to_copy.path
44
+ validate_permalink! page_to_copy.path
45
+
46
+ # TODO: Trigger a page event
47
+ # Bridgetown::Hooks.trigger :pages, :post_init, self
48
+ end
49
+
50
+ # rubocop:disable Naming/AccessorMethodName
51
+ def set_url(url_value)
52
+ @path = url_value.delete_prefix "/"
53
+ @dir = File.dirname(@path)
54
+ @url = url_value
55
+ end
56
+ # rubocop:enable Naming/AccessorMethodName
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Paginate
5
+ module Generator
6
+ #
7
+ # Handles the preparation of all the documents based on the current page index
8
+ #
9
+ class Paginator
10
+ attr_reader :page, :per_page, :documents, :total_documents, :total_pages,
11
+ :previous_page, :previous_page_path, :next_page, :next_page_path, :page_path,
12
+ :first_page, :first_page_path, :last_page, :last_page_path
13
+ attr_accessor :page_trail
14
+
15
+ # Initialize a new Paginator.
16
+ #
17
+ def initialize(
18
+ config_per_page,
19
+ first_index_page_url,
20
+ paginated_page_url,
21
+ documents,
22
+ cur_page_nr,
23
+ num_pages,
24
+ default_indexpage,
25
+ default_ext
26
+ )
27
+ @page = cur_page_nr
28
+ @per_page = config_per_page.to_i
29
+ @total_pages = num_pages
30
+
31
+ if @page > @total_pages
32
+ raise "page number can't be greater than total pages:" \
33
+ " #{@page} > #{@total_pages}"
34
+ end
35
+
36
+ init = (@page - 1) * @per_page
37
+ offset = if init + @per_page - 1 >= documents.size
38
+ documents.size
39
+ else
40
+ init + @per_page - 1
41
+ end
42
+
43
+ # Ensure that the current page has correct extensions if needed
44
+ this_page_url = Utils.ensure_full_path(
45
+ @page == 1 ? first_index_page_url : paginated_page_url,
46
+ !default_indexpage || default_indexpage.empty? ? "index" : default_indexpage,
47
+ !default_ext || default_ext.empty? ? ".html" : default_ext
48
+ )
49
+
50
+ # To support customizable pagination pages we attempt to explicitly
51
+ # append the page name to the url incase the user is using extensionless permalinks.
52
+ if default_indexpage&.length&.positive?
53
+ # Adjust first page url
54
+ first_index_page_url = Utils.ensure_full_path(
55
+ first_index_page_url, default_indexpage, default_ext
56
+ )
57
+ # Adjust the paginated pages as well
58
+ paginated_page_url = Utils.ensure_full_path(
59
+ paginated_page_url, default_indexpage, default_ext
60
+ )
61
+ end
62
+
63
+ @total_documents = documents.size
64
+ @documents = documents[init..offset]
65
+ @page_path = Utils.format_page_number(this_page_url, cur_page_nr, @total_pages)
66
+
67
+ @previous_page = @page != 1 ? @page - 1 : nil
68
+ @previous_page_path = unless @page == 1
69
+ if @page == 2
70
+ Utils.format_page_number(
71
+ first_index_page_url, 1, @total_pages
72
+ )
73
+ else
74
+ Utils.format_page_number(
75
+ paginated_page_url,
76
+ @previous_page,
77
+ @total_pages
78
+ )
79
+ end
80
+ end
81
+ @next_page = @page != @total_pages ? @page + 1 : nil
82
+ @next_page_path = if @page != @total_pages
83
+ Utils.format_page_number(
84
+ paginated_page_url, @next_page, @total_pages
85
+ )
86
+ end
87
+
88
+ @first_page = 1
89
+ @first_page_path = Utils.format_page_number(first_index_page_url, 1, @total_pages)
90
+ @last_page = @total_pages
91
+ @last_page_path = Utils.format_page_number(paginated_page_url, @total_pages, @total_pages)
92
+ end
93
+
94
+ # Convert this Paginator's data to a Hash suitable for use by Liquid.
95
+ #
96
+ # Returns the Hash representation of this Paginator.
97
+ def to_liquid
98
+ {
99
+ "per_page" => per_page,
100
+ "documents" => documents,
101
+ "total_documents" => total_documents,
102
+ "total_pages" => total_pages,
103
+ "page" => page,
104
+ "page_path" => page_path,
105
+ "previous_page" => previous_page,
106
+ "previous_page_path" => previous_page_path,
107
+ "next_page" => next_page,
108
+ "next_page_path" => next_page_path,
109
+ "first_page" => first_page,
110
+ "first_page_path" => first_page_path,
111
+ "last_page" => last_page,
112
+ "last_page_path" => last_page_path,
113
+ "page_trail" => page_trail,
114
+ }
115
+ end
116
+ end
117
+
118
+ # Small utility class that handles individual pagination trails
119
+ # and makes them easier to work with in Liquid
120
+ class PageTrail
121
+ attr_reader :num, :path, :title
122
+
123
+ def initialize(num, path, title)
124
+ @num = num
125
+ @path = path
126
+ @title = title
127
+ end
128
+
129
+ def to_liquid
130
+ {
131
+ "num" => num,
132
+ "path" => path,
133
+ "title" => title,
134
+ }
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Paginate
5
+ module Generator
6
+ # Static utility functions that are used in the code and
7
+ # don't belong in once place in particular
8
+ class Utils
9
+ # Static: Calculate the number of pages.
10
+ #
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) page
20
+ # number and maximum (:max) page count replaced if configured
21
+ #
22
+ def self.format_page_number(to_format, cur_page_nr, total_page_count = nil)
23
+ s = to_format.sub(":num", cur_page_nr.to_s)
24
+ s = s.sub(":max", total_page_count.to_s) unless total_page_count.nil?
25
+
26
+ s
27
+ end
28
+
29
+ # Static: returns a fully formatted string with the :title variable and
30
+ # the current (:num) page number and maximum (:max) page count replaced
31
+ #
32
+ def self.format_page_title(to_format, title, cur_page_nr = nil, total_page_count = nil)
33
+ format_page_number(to_format.sub(":title", title.to_s), cur_page_nr, total_page_count)
34
+ end
35
+
36
+ # Static: Return a String version of the input which has a leading dot.
37
+ # If the input already has a dot in position zero, it will be
38
+ # returned unchanged.
39
+ #
40
+ # path - a String path
41
+ #
42
+ # Returns the path with a leading slash
43
+ def self.ensure_leading_dot(path)
44
+ path[0..0] == "." ? path : ".#{path}"
45
+ end
46
+
47
+ # Static: Return a String version of the input which has a leading slash.
48
+ # If the input already has a forward slash in position zero, it will be
49
+ # returned unchanged.
50
+ #
51
+ # path - a String path
52
+ #
53
+ # Returns the path with a leading slash
54
+ def self.ensure_leading_slash(path)
55
+ path[0..0] == "/" ? path : "/#{path}"
56
+ end
57
+
58
+ # Static: Return a String version of the input without a leading slash.
59
+ #
60
+ # path - a String path
61
+ #
62
+ # Returns the input without the leading slash
63
+ def self.remove_leading_slash(path)
64
+ path[0..0] == "/" ? path[1..-1] : path
65
+ end
66
+
67
+ # Static: Return a String version of the input which has a trailing slash.
68
+ # If the input already has a forward slash at the end, it will be
69
+ # returned unchanged.
70
+ #
71
+ # path - a String path
72
+ #
73
+ # Returns the path with a trailing slash
74
+ def self.ensure_trailing_slash(path)
75
+ path[-1] == "/" ? path : "#{path}/"
76
+ end
77
+
78
+ #
79
+ # Sorting routine used for ordering posts by custom fields.
80
+ # Handles Strings separately as we want a case-insenstive sorting
81
+ #
82
+ # rubocop:disable Naming/MethodParameterName, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
83
+ def self.sort_values(a, b)
84
+ if a.nil? && !b.nil?
85
+ return -1
86
+ elsif !a.nil? && b.nil?
87
+ return 1
88
+ end
89
+
90
+ return a.downcase <=> b.downcase if a.is_a?(String)
91
+
92
+ if a.respond_to?("to_datetime") && b.respond_to?("to_datetime")
93
+ return a.to_datetime <=> b.to_datetime
94
+ end
95
+
96
+ # By default use the built in sorting for the data type
97
+ a <=> b
98
+ end
99
+ # rubocop:enable Naming/MethodParameterName, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
100
+
101
+ # Retrieves the given sort field from the given post
102
+ # the sort_field variable can be a hierarchical value on the form
103
+ # "parent_field:child_field" repeated as many times as needed
104
+ # only the leaf child_field will be retrieved
105
+ def self.sort_get_post_data(post_data, sort_field)
106
+ # Begin by splitting up the sort_field by (;,:.)
107
+ sort_split = sort_field.split(":")
108
+ sort_value = post_data
109
+
110
+ sort_split.each do |r_key|
111
+ key = r_key.downcase.strip # Remove any erronious whitespace and convert to lower case
112
+ return nil unless sort_value.key?(key)
113
+
114
+ # Work my way through the hash
115
+ sort_value = sort_value[key]
116
+ end
117
+
118
+ # If the sort value is a hash then return nil else return the value
119
+ if sort_value.is_a?(Hash)
120
+ nil
121
+ else
122
+ sort_value
123
+ end
124
+ end
125
+
126
+ # Ensures that the passed in url has a index and extension applied
127
+ def self.ensure_full_path(url, default_index, default_ext)
128
+ if url.end_with?("/")
129
+ url + default_index + default_ext
130
+ elsif !url.include?(".")
131
+ url + default_index
132
+ else
133
+ url
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bridgetown-paginate
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - Bridgetown Team
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-04-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bridgetown-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.8.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.8.0
27
+ description:
28
+ email: maintainers@bridgetownrb.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - ".rspec"
34
+ - ".rubocop.yml"
35
+ - Rakefile
36
+ - bridgetown-paginate.gemspec
37
+ - lib/bridgetown-paginate.rb
38
+ - lib/bridgetown-paginate/defaults.rb
39
+ - lib/bridgetown-paginate/pagination_generator.rb
40
+ - lib/bridgetown-paginate/pagination_indexer.rb
41
+ - lib/bridgetown-paginate/pagination_model.rb
42
+ - lib/bridgetown-paginate/pagination_page.rb
43
+ - lib/bridgetown-paginate/paginator.rb
44
+ - lib/bridgetown-paginate/utils.rb
45
+ homepage: https://github.com/bridgetownrb/bridgetown-paginate
46
+ licenses:
47
+ - MIT
48
+ metadata: {}
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 3.0.6
65
+ signing_key:
66
+ specification_version: 4
67
+ summary: A Bridgetown plugin to add pagination support for posts and collection indices.
68
+ test_files: []