j1-paginator 2020.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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