bridgetown-paginate 0.8.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.
@@ -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: []