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,407 @@
1
+ module Jekyll
2
+ module J1Paginator::Generator
3
+
4
+ #
5
+ # The main model for the pagination, handles the orchestration of the
6
+ # pagination and calling all the necessary bits and bobs needed :)
7
+ #
8
+ class PaginationModel
9
+
10
+ @debug = false # is debug output enabled?
11
+ @logging_lambda = nil # The lambda to use for logging
12
+ @page_add_lambda = nil # The lambda used to create pages and add them to the site
13
+ @page_remove_lambda = nil # Lambda to remove a page from the site.pages collection
14
+ @collection_by_name_lambda = nil # Lambda to get all documents/posts in a particular collection (by name)
15
+
16
+ # ctor
17
+ def initialize(logging_lambda, page_add_lambda, page_remove_lambda, collection_by_name_lambda)
18
+ @logging_lambda = logging_lambda
19
+ @page_add_lambda = page_add_lambda
20
+ @page_remove_lambda = page_remove_lambda
21
+ @collection_by_name_lambda = collection_by_name_lambda
22
+ end
23
+
24
+ def run(default_config, site_pages, site_title)
25
+ # By default if pagination is enabled we attempt to find all index.html
26
+ # pages in the site
27
+ templates = self.discover_paginate_templates(site_pages)
28
+ if templates.size <= 0
29
+ @logging_lambda.call "Is enabled, but I couldn't find any pagination page. Skipping pagination. "+
30
+ "Pages must have 'pagination: enabled: true' in their front-matter for pagination to work.", "warn"
31
+ return
32
+ end
33
+
34
+ # Now for each template page generate the paginator for it
35
+ templates.each do |template|
36
+ # All pages that should be paginated need to include the pagination
37
+ # config element
38
+ if template.data['pagination'].is_a?(Hash)
39
+ template_config = Jekyll::Utils.deep_merge_hashes(default_config, template.data['pagination'] || {})
40
+
41
+ # Handling deprecation of configuration values
42
+ self._fix_deprecated_config_features(template_config)
43
+
44
+ # Is debugging enabled on the page level?
45
+ @debug = template_config['debug']
46
+
47
+ self._debug_print_config_info(template_config, template.path)
48
+
49
+ # Only paginate the template if it is explicitly enabled
50
+ # This makes the logic simpler by avoiding the need to determine
51
+ # which index pages were generated automatically and which weren't
52
+ if template_config['enabled'].to_s == 'true'
53
+ if !@debug
54
+ @logging_lambda.call "found page: "+template.path, 'debug'
55
+ end
56
+
57
+ # Request all documents in all collections that the user has
58
+ # requested
59
+ all_posts = self.get_docs_in_collections(template_config['collection'])
60
+
61
+ # Create the necessary indexes for the posts
62
+ # Populate a category for all posts
63
+ all_categories = PaginationIndexer.index_posts_by(all_posts, 'categories')
64
+
65
+ # Populate a category for all posts (this is here for backward
66
+ # compatibility, do not use this as it will be decommissioned 2018-01-01)
67
+ # (this is a default and must not be used in the category system)
68
+ # all_categories['posts'] = all_posts;
69
+
70
+ all_tags = PaginationIndexer.index_posts_by(all_posts, 'tags')
71
+ all_locales = PaginationIndexer.index_posts_by(all_posts, 'locale')
72
+
73
+ # TODO: NOTE!!! This whole request for posts and indexing
74
+ # results could be cached to improve performance, leaving
75
+ # like this for now during testing
76
+
77
+ # Now construct the pagination data for this template page
78
+ self.paginate(template, template_config, site_title, all_posts, all_tags, all_categories, all_locales)
79
+ end
80
+ end
81
+ end #for
82
+
83
+ # Return the total number of templates found
84
+ return templates.size
85
+ end # function run
86
+
87
+ # This function is here to retain the old compatability logic with the
88
+ # jekyll-paginate gem no changes should be made to this function and
89
+ # it should be retired and deleted after 2018-01-01
90
+ #
91
+ # def run_compatability(legacy_config, site_pages, site_title, all_posts)
92
+ #
93
+ # # Decomissioning error
94
+ # if( date = Date.strptime("20180101","%Y%m%d") <= Date.today )
95
+ # raise ArgumentError.new("Legacy jekyll-paginate configuration compatibility mode has expired. Please upgrade to j1-paginator configuration.")
96
+ # end
97
+ #
98
+ # # Two month warning or general notification
99
+ # if( date = Date.strptime("20171101","%Y%m%d") <= Date.today )
100
+ # @logging_lambda.call "Legacy pagination logic will stop working on Jan 1st 2018, update your configs before that time.", "warn"
101
+ # else
102
+ # @logging_lambda.call "Detected legacy jekyll-paginate logic running. "+
103
+ # "Please update your configs to use the j1-paginator logic. This compatibility function "+
104
+ # "will stop working after Jan 1st 2018 and your site build will throw an error.", "warn"
105
+ # end
106
+ #
107
+ # if template = CompatibilityUtils.template_page(site_pages, legacy_config['legacy_source'], legacy_config['permalink'])
108
+ # CompatibilityUtils.paginate(legacy_config, all_posts, template, @page_add_lambda)
109
+ # else
110
+ # @logging_lambda.call "Legacy pagination is enabled, but I couldn't find " +
111
+ # "an index.html page to use as the pagination page. Skipping pagination.", "warn"
112
+ # end
113
+ # end # function run_compatability (REMOVE AFTER 2018-01-01)
114
+
115
+ # Returns the combination of all documents in the collections that are
116
+ # specified. raw_collection_names can either be a list of collections
117
+ # separated by a ',' or ' ' or a single string
118
+ def get_docs_in_collections(raw_collection_names)
119
+ if raw_collection_names.is_a?(String)
120
+ collection_names = raw_collection_names.split(/;|,|\s/)
121
+ else
122
+ collection_names = raw_collection_names
123
+ end
124
+
125
+ docs = []
126
+ # Now for each of the collections get the docs
127
+ collection_names.each do |coll_name|
128
+ # Request all the documents for the collection in question, and
129
+ # join it with the total collection
130
+ docs += @collection_by_name_lambda.call(coll_name.downcase.strip)
131
+ end
132
+
133
+ # Hidden documents should not not be processed anywhere.
134
+ docs = docs.reject { |doc| doc['hidden'] }
135
+
136
+ return docs
137
+ end
138
+
139
+ def _fix_deprecated_config_features(config)
140
+ keys_to_delete = []
141
+
142
+ # As of v1.5.1 the title_suffix is deprecated and 'title' should be used
143
+ # but only if title has not been defined already!
144
+ if( !config['title_suffix'].nil? )
145
+ if( config['title'].nil? )
146
+ config['title'] = ":title" + config['title_suffix'].to_s # Migrate the old key to title
147
+ end
148
+ keys_to_delete << "title_suffix" # Always remove the deprecated key if found
149
+ end
150
+
151
+ # Delete the depricated keys
152
+ config.delete_if{ |k,| keys_to_delete.include? k }
153
+ end
154
+
155
+ LOG_KEY = 'J1 Paginator: '.rjust(20).freeze
156
+ DIVIDER = ('-' * 80).freeze
157
+ NOT_SET = '[Not set]'.freeze
158
+
159
+ # Debug print the config
160
+ def _debug_log(topic, message = nil)
161
+ return unless @debug
162
+
163
+ message = message.to_s
164
+ topic = "#{topic.ljust(24)}: " unless message.empty?
165
+ puts LOG_KEY + topic + message
166
+ end
167
+
168
+ # Debug print the config
169
+ def _debug_print_config_info(config, page_path)
170
+ return unless @debug
171
+
172
+ puts ''
173
+ puts LOG_KEY + "Page: #{page_path}"
174
+ puts LOG_KEY + DIVIDER
175
+ _debug_log ' Active configuration'
176
+ _debug_log ' Enabled', config['enabled']
177
+ _debug_log ' Items per page', config['per_page']
178
+ _debug_log ' Permalink', config['permalink']
179
+ _debug_log ' Title', config['title']
180
+ _debug_log ' Limit', config['limit']
181
+ _debug_log ' Sort by', config['sort_field']
182
+ _debug_log ' Sort reverse', config['sort_reverse']
183
+ _debug_log ' Active Filters'
184
+ _debug_log ' Collection', config['collection']
185
+ _debug_log ' Offset', config['offset']
186
+ _debug_log ' Category', (config['category'].nil? || config['category'] == 'posts' ? NOT_SET : config['category'])
187
+ _debug_log ' Tag', config['tag'] || NOT_SET
188
+ _debug_log ' Locale', config['locale'] || NOT_SET
189
+
190
+ return unless config['legacy']
191
+
192
+ _debug_log ' Legacy Paginate Code Enabled'
193
+ _debug_log ' Legacy Paginate', config['per_page']
194
+ _debug_log ' Legacy Source', config['legacy_source']
195
+ _debug_log ' Legacy Path', config['paginate_path']
196
+ end
197
+
198
+ # Debug print the config
199
+ def _debug_print_filtering_info(filter_name, before_count, after_count)
200
+ return unless @debug
201
+
202
+ filter_name = filter_name.to_s.ljust(9)
203
+ before_count = before_count.to_s.rjust(3)
204
+ _debug_log " Filtering by #{filter_name}", "#{before_count} => #{after_count}"
205
+ end
206
+
207
+ #
208
+ # Rolls through all the pages passed in and finds all pages that have
209
+ # pagination enabled on them. These pages will be used as templates
210
+ #
211
+ # site_pages - All pages in the site
212
+ #
213
+ def discover_paginate_templates(site_pages)
214
+ candidates = []
215
+ site_pages.select do |page|
216
+ # If the page has the enabled config set, supports any type of
217
+ # file name html or md
218
+ if page.data['pagination'].is_a?(Hash) && page.data['pagination']['enabled']
219
+ candidates << page
220
+ end
221
+ end
222
+ return candidates
223
+ end # function discover_paginate_templates
224
+
225
+ # Paginates the blog's posts. Renders the index.html file into paginated
226
+ # directories, e.g.: page2/index.html, page3/index.html, etc and adds more
227
+ # site-wide data.
228
+ #
229
+ # site - The Site.
230
+ # template - The index.html Page that requires pagination.
231
+ # config - The configuration settings that should be used
232
+ #
233
+ def paginate(template, config, site_title, all_posts, all_tags, all_categories, all_locales)
234
+ # By default paginate on all posts in the site
235
+ using_posts = all_posts
236
+
237
+ # Now start filtering out any posts that the user doesn't want included in the pagination
238
+ before = using_posts.size
239
+ using_posts = PaginationIndexer.read_config_value_and_filter_posts(config, 'category', using_posts, all_categories)
240
+ self._debug_print_filtering_info('Category', before, using_posts.size)
241
+ before = using_posts.size
242
+ using_posts = PaginationIndexer.read_config_value_and_filter_posts(config, 'tag', using_posts, all_tags)
243
+ self._debug_print_filtering_info('Tag', before, using_posts.size)
244
+ before = using_posts.size
245
+ using_posts = PaginationIndexer.read_config_value_and_filter_posts(config, 'locale', using_posts, all_locales)
246
+ self._debug_print_filtering_info('Locale', before, using_posts.size)
247
+
248
+ # Apply sorting to the posts if configured, any field for the post
249
+ # is available for sorting
250
+ if config['sort_field']
251
+ sort_field = config['sort_field'].to_s
252
+
253
+ # There is an issue in Jekyll related to lazy initialized member
254
+ # variables that causes iterators to break when accessing an
255
+ # uninitialized value during iteration. This happens for document.rb
256
+ # when the <=> compaison function is called (as this function
257
+ # calls the 'date' field which for drafts are not initialized.)
258
+ # So to unblock this common issue for the date field I simply
259
+ # iterate once over every document and initialize the .date
260
+ # field explicitly
261
+ if @debug
262
+ Jekyll.logger.info "J1 Paginator:", "Rolling through the date fields for all documents"
263
+ end
264
+ using_posts.each do |u_post|
265
+ if u_post.respond_to?('date')
266
+ tmp_date = u_post.date
267
+ if( !tmp_date || tmp_date.nil? )
268
+ if @debug
269
+ Jekyll.logger.info "J1 Paginator:", "Explicitly assigning date for doc: #{u_post.data['title']} | #{u_post.path}"
270
+ end
271
+ u_post.date = File.mtime(u_post.path)
272
+ end
273
+ end
274
+ end
275
+
276
+ using_posts.sort!{ |a,b| Utils.sort_values(Utils.sort_get_post_data(a.data, sort_field), Utils.sort_get_post_data(b.data, sort_field)) }
277
+
278
+ # Remove the first x entries
279
+ offset_post_count = [0, config['offset'].to_i].max
280
+ using_posts.pop(offset_post_count)
281
+
282
+ if config['sort_reverse']
283
+ using_posts.reverse!
284
+ end
285
+ end
286
+
287
+ # Calculate the max number of pagination-pages based on the configured
288
+ # per page value
289
+ total_pages = Utils.calculate_number_of_pages(using_posts, config['per_page'])
290
+
291
+ # If a upper limit is set on the number of total pagination pages
292
+ # then impose that now
293
+ if config['limit'] && config['limit'].to_i > 0 && config['limit'].to_i < total_pages
294
+ total_pages = config['limit'].to_i
295
+ end
296
+
297
+ #### BEFORE STARTING REMOVE THE TEMPLATE PAGE FROM THE SITE LIST!
298
+ @page_remove_lambda.call( template )
299
+
300
+ # list of all newly created pages
301
+ newpages = []
302
+
303
+ # Consider the default index page name and extension
304
+ indexPageName = config['indexpage'].nil? ? '' : config['indexpage'].split('.')[0]
305
+ indexPageExt = config['extension'].nil? ? '' : Utils.ensure_leading_dot(config['extension'])
306
+ indexPageWithExt = indexPageName + indexPageExt
307
+
308
+ # In case there are no (visible) posts, generate the index file anyway
309
+ total_pages = 1 if total_pages.zero?
310
+
311
+ # Now for each pagination page create it and configure the ranges
312
+ # for the collection. This .pager member is a built in thing in Jekyll
313
+ # and defines the paginator implementation. Simply override to use mine
314
+ (1..total_pages).each do |cur_page_nr|
315
+
316
+ # 1. Create the in-memory page
317
+ # External Proc call to create the actual page for us (this is passed in when the pagination is run)
318
+ newpage = PaginationPage.new( template, cur_page_nr, total_pages, indexPageWithExt )
319
+
320
+ # 2. Create the url for the in-memory page (calc permalink etc),
321
+ # construct the title, set all page.data values needed
322
+ first_index_page_url = Utils.validate_url(template)
323
+ paginated_page_url = File.join(first_index_page_url, config['permalink'])
324
+
325
+ # 3. Create the pager logic for this page, pass in the prev and
326
+ # next page numbers, assign pager to in-memory page
327
+ newpage.pager = Paginator.new( config['per_page'], first_index_page_url, paginated_page_url, using_posts, cur_page_nr, total_pages, indexPageName, indexPageExt)
328
+
329
+ # Create the url for the new page, make sure we prepend any
330
+ # permalinks that are defined in the template page before
331
+ pager_path = newpage.pager.page_path
332
+ if pager_path.end_with? '/'
333
+ newpage.url = File.join(pager_path, indexPageWithExt)
334
+ elsif pager_path.end_with? indexPageExt
335
+ # Support for direct .html files
336
+ newpage.url = pager_path
337
+ else
338
+ # Support for extensionless permalinks
339
+ newpage.url = pager_path + indexPageExt
340
+ end
341
+
342
+ if( template.data['permalink'] )
343
+ newpage.data['permalink'] = pager_path
344
+ end
345
+
346
+ # Transfer the title across to the new page
347
+ tmp_title = template.data['title'] || site_title
348
+ if cur_page_nr > 1 && config.has_key?('title')
349
+ # If the user specified a title suffix to be added then let's
350
+ # add that to all the pages except the first
351
+ newpage.data['title'] = "#{Utils.format_page_title(config['title'], tmp_title, cur_page_nr, total_pages)}"
352
+ else
353
+ newpage.data['title'] = tmp_title
354
+ end
355
+
356
+ # Signals that this page is automatically generated by the
357
+ # pagination logic (don't do this for the first page as it is
358
+ # there to mask the one we removed)
359
+ if cur_page_nr > 1
360
+ newpage.data['autogen'] = "j1-paginator"
361
+ end
362
+
363
+ # Add the page to the site
364
+ @page_add_lambda.call( newpage )
365
+
366
+ # Store the page in an internal list for later referencing if we
367
+ # need to generate a pagination number path later on
368
+ newpages << newpage
369
+ end #each.do total_pages
370
+
371
+ # Now generate the pagination number path, e.g. so that the users
372
+ # can have a prev 1 2 3 4 5 next structure on their page
373
+ # simplest is to include all of the links to the pages preceeding
374
+ # the current one (e.g for page 1 you get the list 2, 3, 4.... and
375
+ # for page 2 you get the list 3,4,5...)
376
+ if config['trail'] && newpages.size > 1
377
+ trail_before = [config['trail']['before'].to_i, 0].max
378
+ trail_after = [config['trail']['after'].to_i, 0].max
379
+ trail_length = trail_before + trail_after + 1
380
+
381
+ if( trail_before > 0 || trail_after > 0 )
382
+ newpages.select do | npage |
383
+ idx_start = [ npage.pager.page - trail_before - 1, 0].max # Selecting the beginning of the trail
384
+ idx_end = [idx_start + trail_length, newpages.size].min # Selecting the end of the trail
385
+
386
+ # Always attempt to maintain the max total of <trail_length>
387
+ # pages in the trail (it will look better if the trail doesn't shrink)
388
+ if( idx_end - idx_start < trail_length )
389
+ # Attempt to pad the beginning if we have enough pages
390
+ idx_start = [idx_start - ( trail_length - (idx_end - idx_start) ), 0].max # Never go beyond the zero index
391
+ end
392
+
393
+ # Convert the newpages array into a two dimensional array
394
+ # that has [index, page_url] as items
395
+ #puts( "Trail created for page #{npage.pager.page} (idx_start:#{idx_start} idx_end:#{idx_end})")
396
+ npage.pager.page_trail = newpages[idx_start...idx_end].each_with_index.map {|ipage,idx| PageTrail.new(idx_start+idx+1, ipage.pager.page_path, ipage.data['title'])}
397
+ #puts( npage.pager.page_trail )
398
+ end #newpages.select
399
+ end #if trail_before / trail_after
400
+ end # if config['trail']
401
+
402
+ end # function paginate
403
+
404
+ end # class PaginationV2
405
+
406
+ end # module J1Paginator
407
+ end # module Jekyll
@@ -0,0 +1,70 @@
1
+ module Jekyll
2
+ module J1Paginator::Generator
3
+
4
+ #
5
+ # This page handles the creation of the fake pagination pages based
6
+ # on the original page configuration. The code does the same things as
7
+ # the default Jekyll/page.rb code but just forces the code to look
8
+ # into the template instead of the (currently non-existing)
9
+ # pagination page.
10
+ #
11
+ # NOTE: This page exists purely in memory and is not read from disk
12
+ #
13
+ class PaginationPage < Page
14
+ attr_reader :relative_path
15
+
16
+ def initialize(page_to_copy, cur_page_nr, total_pages, index_pageandext)
17
+ @site = page_to_copy.site
18
+ @base = ''
19
+ @url = ''
20
+ @relative_path = page_to_copy.relative_path
21
+
22
+ if cur_page_nr == 1
23
+ @dir = File.dirname(page_to_copy.dir)
24
+ @name = page_to_copy.name
25
+ else
26
+ @name = index_pageandext.nil? ? 'index.html' : index_pageandext
27
+ end
28
+
29
+ self.process(@name) # Creates the basename and ext member values
30
+
31
+ # Copy page data over site defaults
32
+ defaults = @site.frontmatter_defaults.all(page_to_copy.relative_path, type)
33
+ self.data = Jekyll::Utils.deep_merge_hashes(defaults, page_to_copy.data)
34
+
35
+ if defaults.has_key?('permalink')
36
+ self.data['permalink'] = Jekyll::URL.new(:template => defaults['permalink'], :placeholders => self.url_placeholders).to_s
37
+ @use_permalink_for_url = true
38
+ end
39
+
40
+ if !page_to_copy.data['autopage']
41
+ self.content = page_to_copy.content
42
+ else
43
+ # If the page is an auto page then migrate the necessary autopage
44
+ # info across into the new pagination page (so that users can get
45
+ # the correct keys etc)
46
+ if( page_to_copy.data['autopage'].has_key?('display_name') )
47
+ self.data['autopages'] = Jekyll::Utils.deep_merge_hashes( page_to_copy.data['autopage'], {} )
48
+ end
49
+ end
50
+
51
+ # Store the current page and total page numbers in the pagination_info
52
+ # construct
53
+ self.data['pagination_info'] = {"curr_page" => cur_page_nr, 'total_pages' => total_pages }
54
+
55
+ # Perform some validation that is also performed in Jekyll::Page
56
+ validate_data! page_to_copy.path
57
+ validate_permalink! page_to_copy.path
58
+
59
+ # Trigger a page event
60
+ #Jekyll::Hooks.trigger :pages, :post_init, self
61
+ end
62
+
63
+ def url=(url_value)
64
+ @url = @use_permalink_for_url ? self.data['permalink'] : url_value
65
+ end
66
+ alias_method :set_url, :url=
67
+ end # class PaginationPage
68
+
69
+ end # module J1Paginator
70
+ end # module Jekyll