j1_paginator 2019.1.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,76 @@
1
+ module Jekyll
2
+ module J1Paginator::AutoPages
3
+
4
+ class Utils
5
+
6
+ # Static: returns a fully formatted string with the tag macro (:tag) replaced
7
+ #
8
+ def self.format_tag_macro(toFormat, tag, slugify_config=nil)
9
+ slugify_mode = slugify_config.has_key?('mode') ? slugify_config['mode'] : nil
10
+ slugify_cased = slugify_config.has_key?('cased') ? slugify_config['cased'] : false
11
+ return toFormat.sub(':tag', Jekyll::Utils.slugify(tag.to_s, mode:slugify_mode, cased:slugify_cased))
12
+ end #function format_tag_macro
13
+
14
+ # Static: returns a fully formatted string with the category macro (:cat) replaced
15
+ #
16
+ def self.format_cat_macro(toFormat, category, slugify_config=nil)
17
+ slugify_mode = slugify_config.has_key?('mode') ? slugify_config['mode'] : nil
18
+ slugify_cased = slugify_config.has_key?('cased') ? slugify_config['cased'] : false
19
+ return toFormat.sub(':cat', Jekyll::Utils.slugify(category.to_s, mode:slugify_mode, cased:slugify_cased))
20
+ end #function format_cat_macro
21
+
22
+ # Static: returns a fully formatted string with the collection macro (:coll) replaced
23
+ #
24
+ def self.format_coll_macro(toFormat, collection, slugify_config=nil)
25
+ slugify_mode = slugify_config.has_key?('mode') ? slugify_config['mode'] : nil
26
+ slugify_cased = slugify_config.has_key?('cased') ? slugify_config['cased'] : false
27
+ return toFormat.sub(':coll', Jekyll::Utils.slugify(collection.to_s, mode:slugify_mode, cased:slugify_cased))
28
+ end #function format_coll_macro
29
+
30
+ # Static: returns all documents from all collections defined in the hash of collections passed in
31
+ # excludes all pagination pages though
32
+ def self.collect_all_docs(site_collections)
33
+ coll = []
34
+ site_collections.each do |coll_name, coll_data|
35
+ if !coll_data.nil?
36
+ coll += coll_data.docs.select { |doc| !doc.data.has_key?('pagination') }.each{ |doc| doc.data['__coll'] = coll_name } # Exclude all pagination pages and then for every page store it's collection name
37
+ end
38
+ end
39
+ return coll
40
+ end
41
+
42
+ def self.ap_index_posts_by(all_posts, index_key)
43
+ return nil if all_posts.nil?
44
+ return all_posts if index_key.nil?
45
+ index = {}
46
+ all_posts.each do |post|
47
+ next if post.data.nil?
48
+ next if !post.data.has_key?(index_key)
49
+ next if post.data[index_key].nil?
50
+ next if post.data[index_key].size <= 0
51
+ next if post.data[index_key].to_s.strip.length == 0
52
+
53
+ # Only tags and categories come as premade arrays, locale does not, so convert any data
54
+ # elements that are strings into arrays
55
+ post_data = post.data[index_key]
56
+ if post_data.is_a?(String)
57
+ post_data = post_data.split(/;|,|\s/)
58
+ end
59
+
60
+ post_data.each do |key|
61
+ key = key.to_s.strip
62
+ processed_key = key.downcase #Clean whitespace and junk
63
+ if !index.has_key?(processed_key)
64
+ # Need to store the original key value here so that I can present it to the users as a page variable they can use (unmodified, e.g. tags not being 'sci-fi' but "Sci-Fi")
65
+ # Also, only interested in storing all the keys not the pages in this case
66
+ index[processed_key] = [processed_key, key]
67
+ end
68
+ end
69
+ end
70
+ return index
71
+ end # function index_posts_by
72
+
73
+ end # class Utils
74
+
75
+ end # module J1Paginator
76
+ end # module Jekyll
@@ -0,0 +1,121 @@
1
+ module Jekyll
2
+ module J1Paginator::Generator
3
+
4
+ class CompatibilityPaginationPage < Page
5
+ def initialize(site, base, dir, template_path)
6
+ @site = site
7
+ @base = base
8
+ @dir = dir
9
+ @template = template_path
10
+ @name = 'index.html'
11
+
12
+ templ_dir = File.dirname(template_path)
13
+ templ_file = File.basename(template_path)
14
+
15
+ # Path is only used by the convertible module and accessed below when calling read_yaml
16
+ # in our case we have the path point to the original template instead of our faux new pagination page
17
+ @path = if site.in_theme_dir(base) == base # we're in a theme
18
+ site.in_theme_dir(base, templ_dir, templ_file)
19
+ else
20
+ site.in_source_dir(base, templ_dir, templ_file)
21
+ end
22
+
23
+ self.process(@name)
24
+ self.read_yaml(templ_dir, templ_file)
25
+
26
+ data.default_proc = proc do |_, key|
27
+ site.frontmatter_defaults.find(File.join(templ_dir, templ_file), type, key)
28
+ end
29
+
30
+ end
31
+ end # class CompatibilityPaginationPage
32
+
33
+ #
34
+ # Static utility functions that provide backwards compatibility with the old
35
+ # jekyll-paginate gem that this new version superseeds (this code is here to ensure)
36
+ # that sites still running the old gem work without problems
37
+ # (REMOVE AFTER 2018-01-01)
38
+ #
39
+ # THIS CLASS IS ADAPTED FROM THE ORIGINAL IMPLEMENTATION AND WILL BE REMOVED, THERE ARE DELIBERATELY NO TESTS FOR THIS CLASS
40
+ #
41
+ class CompatibilityUtils
42
+
43
+ # Public: Find the Jekyll::Page which will act as the pager template
44
+ #
45
+ # Returns the Jekyll::Page which will act as the pager template
46
+ def self.template_page(site_pages, config_source, config_paginate_path)
47
+ site_pages.select do |page|
48
+ CompatibilityUtils.pagination_candidate?(config_source, config_paginate_path, page)
49
+ end.sort do |one, two|
50
+ two.path.size <=> one.path.size
51
+ end.first
52
+ end
53
+
54
+ # Static: Determine if a page is a possible candidate to be a template page.
55
+ # Page's name must be `index.html` and exist in any of the directories
56
+ # between the site source and `paginate_path`.
57
+ def self.pagination_candidate?(config_source, config_paginate_path, page)
58
+ page_dir = File.dirname(File.expand_path(Utils.remove_leading_slash(page.path), config_source))
59
+ paginate_path = Utils.remove_leading_slash(config_paginate_path)
60
+ paginate_path = File.expand_path(paginate_path, config_source)
61
+ page.name == 'index.html' && CompatibilityUtils.in_hierarchy(config_source, page_dir, File.dirname(paginate_path))
62
+ end
63
+
64
+ # Determine if the subdirectories of the two paths are the same relative to source
65
+ #
66
+ # source - the site source
67
+ # page_dir - the directory of the Jekyll::Page
68
+ # paginate_path - the absolute paginate path (from root of FS)
69
+ #
70
+ # Returns whether the subdirectories are the same relative to source
71
+ def self.in_hierarchy(source, page_dir, paginate_path)
72
+ return false if paginate_path == File.dirname(paginate_path)
73
+ return false if paginate_path == Pathname.new(source).parent
74
+ page_dir == paginate_path ||
75
+ CompatibilityUtils.in_hierarchy(source, page_dir, File.dirname(paginate_path))
76
+ end
77
+
78
+ # Paginates the blog's posts. Renders the index.html file into paginated
79
+ # directories, e.g.: page2/index.html, page3/index.html, etc and adds more
80
+ # site-wide data.
81
+ #
82
+ def self.paginate(legacy_config, all_posts, page, page_add_lambda )
83
+ pages = Utils.calculate_number_of_pages(all_posts, legacy_config['per_page'].to_i)
84
+ (1..pages).each do |num_page|
85
+ pager = Paginator.new( legacy_config['per_page'], page.url, legacy_config['permalink'], all_posts, num_page, pages, '', '' )
86
+ if num_page > 1
87
+ template_full_path = File.join(page.site.source, page.path)
88
+ template_dir = File.dirname(page.path)
89
+ newpage = CompatibilityPaginationPage.new(page.site, page.site.source, template_dir, template_full_path)
90
+ newpage.pager = pager
91
+ newpage.dir = CompatibilityUtils.paginate_path(page.url, num_page, legacy_config['permalink'])
92
+ newpage.data['autogen'] = "j1_paginator" # Signals that this page is automatically generated by the pagination logic
93
+ page_add_lambda.call(newpage)
94
+ else
95
+ page.pager = pager
96
+ end
97
+ end
98
+ end
99
+
100
+ # Static: Return the pagination path of the page
101
+ #
102
+ # site - the Jekyll::Site object
103
+ # cur_page_nr - the pagination page number
104
+ # config - the current configuration in use
105
+ #
106
+ # Returns the pagination path as a string
107
+ def self.paginate_path(template_url, cur_page_nr, permalink_format)
108
+ return nil if cur_page_nr.nil?
109
+ return template_url if cur_page_nr <= 1
110
+ if permalink_format.include?(":num")
111
+ permalink_format = Utils.format_page_number(permalink_format, cur_page_nr)
112
+ else
113
+ raise ArgumentError.new("Invalid pagination path: '#{permalink_format}'. It must include ':num'.")
114
+ end
115
+
116
+ Utils.ensure_leading_slash(permalink_format)
117
+ end #function paginate_path
118
+
119
+ end # class CompatibilityUtils
120
+ end # module J1Paginator
121
+ end # module Jekyll
@@ -0,0 +1,27 @@
1
+ module Jekyll
2
+ module J1Paginator::Generator
3
+
4
+ # The default configuration for the Paginator
5
+ DEFAULT = {
6
+ 'enabled' => false,
7
+ 'collection' => 'posts',
8
+ 'offset' => 0, # Supports skipping x number of posts from the beginning of the post list
9
+ 'per_page' => 10,
10
+ 'permalink' => '/page:num/', # Supports :num as customizable elements
11
+ 'title' => ':title - page :num', # Supports :num as customizable elements
12
+ 'page_num' => 1,
13
+ 'sort_reverse' => false,
14
+ 'sort_field' => 'date',
15
+ 'limit' => 0, # Limit how many content objects to paginate (default: 0, means all)
16
+ 'trail' => {
17
+ 'before' => 0, # Limits how many links to show before the current page in the pagination trail (0, means off, default: 0)
18
+ 'after' => 0, # Limits how many links to show after the current page in the pagination trail (0 means off, default: 0)
19
+ },
20
+ 'indexpage' => 'index', # The default name of the index pages
21
+ 'extension' => 'html', # The default extension for the output pages (ignored if indexpage is nil)
22
+ 'debug' => false, # Turns on debug output for the gem
23
+ 'legacy' => false # Internal value, do not use (will be removed after 2018-01-01)
24
+ }
25
+
26
+ end # module J1Paginator
27
+ end # module Jekyll
@@ -0,0 +1,146 @@
1
+ module Jekyll
2
+ module J1Paginator::Generator
3
+
4
+ #
5
+ # The main entry point into the generator, called by Jekyll
6
+ # this function extracts all the necessary information from the jekyll end and passes it into the pagination
7
+ # logic. Additionally it also contains all site specific actions that the pagination logic needs access to
8
+ # (such as how to create new pages)
9
+ #
10
+ class PaginationGenerator < Generator
11
+ # This generator is safe from arbitrary code execution.
12
+ safe true
13
+
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
+ #begin
23
+ # Generate the AutoPages first
24
+ J1Paginator::AutoPages.create_autopages(site)
25
+
26
+ # Retrieve and merge the pagination configuration from the site yml file
27
+ default_config = Jekyll::Utils.deep_merge_hashes(DEFAULT, site.config['pagination'] || {})
28
+
29
+ # Compatibility Note: (REMOVE AFTER 2018-01-01)
30
+ # If the legacy paginate logic is configured then read those values and merge with config
31
+ if !site.config['paginate'].nil?
32
+ Jekyll.logger.info "Pagination:","Legacy paginate configuration settings detected and will be used."
33
+ # You cannot run both the new code and the old code side by side
34
+ if !site.config['pagination'].nil?
35
+ err_msg = "The new j1_paginator and the old jekyll-paginate logic cannot both be configured in the site config at the same time. Please disable the old 'paginate:' config settings by either omitting the values or setting them to 'paginate:off'."
36
+ Jekyll.logger.error err_msg
37
+ raise ArgumentError.new(err_msg)
38
+ end
39
+
40
+ default_config['per_page'] = site.config['paginate'].to_i
41
+ default_config['legacy_source'] = site.config['source']
42
+ if !site.config['paginate_path'].nil?
43
+ default_config['permalink'] = site.config['paginate_path'].to_s
44
+ end
45
+ # In case of legacy, enable pagination by default
46
+ default_config['enabled'] = true
47
+ default_config['legacy'] = true
48
+ end # Compatibility END (REMOVE AFTER 2018-01-01)
49
+
50
+ # If disabled then simply quit
51
+ if !default_config['enabled']
52
+ Jekyll.logger.info "Pagination:","Disabled in site.config."
53
+ return
54
+ end
55
+
56
+ # Handle deprecation of settings and features
57
+ if( !default_config['title_suffix' ].nil? )
58
+ Jekyll::Deprecator.deprecation_message "Pagination: The 'title_suffix' configuration has been deprecated. Please use 'title'. See https://github.com/sverrirs/j1_paginator/blob/master/README-GENERATOR.md#site-configuration"
59
+ end
60
+
61
+ Jekyll.logger.debug "Pagination:","Starting"
62
+
63
+ ################ 0 ####################
64
+ # Get all pages in the site (this will be used to find the pagination templates)
65
+ all_pages = site.pages
66
+
67
+ # Get the default title of the site (used as backup when there is no title available for pagination)
68
+ site_title = site.config['title']
69
+
70
+ ################ 1 ####################
71
+ # Specify the callback function that returns the correct docs/posts based on the collection name
72
+ # "posts" are just another collection in Jekyll but a specialized version that require timestamps
73
+ # This collection is the default and if the user doesn't specify a collection in their front-matter then that is the one we load
74
+ # If the collection is not found then empty array is returned
75
+ collection_by_name_lambda = lambda do |collection_name|
76
+ coll = []
77
+ if collection_name == "all"
78
+ # the 'all' collection_name is a special case and includes all collections in the site (except posts!!)
79
+ # this is useful when you want to list items across multiple collections
80
+ site.collections.each do |coll_name, coll_data|
81
+ if( !coll_data.nil? && coll_name != 'posts')
82
+ coll += coll_data.docs.select { |doc| !doc.data.has_key?('pagination') } # Exclude all pagination pages
83
+ end
84
+ end
85
+ else
86
+ # Just the one collection requested
87
+ if !site.collections.has_key?(collection_name)
88
+ return []
89
+ end
90
+
91
+ coll = site.collections[collection_name].docs.select { |doc| !doc.data.has_key?('pagination') } # Exclude all pagination pages
92
+ end
93
+ return coll
94
+ end
95
+
96
+ ################ 2 ####################
97
+ # Create the proc that constructs the real-life site page
98
+ # This is necessary to decouple the code from the Jekyll site object
99
+ page_add_lambda = lambda do | newpage |
100
+ site.pages << newpage # Add the page to the site so that it is generated correctly
101
+ return newpage # Return the site to the calling code
102
+ end
103
+
104
+ ################ 2.5 ####################
105
+ # lambda that removes a page from the site pages list
106
+ page_remove_lambda = lambda do | page_to_remove |
107
+ site.pages.delete_if {|page| page == page_to_remove }
108
+ end
109
+
110
+ ################ 3 ####################
111
+ # Create a proc that will delegate logging
112
+ # Decoupling Jekyll specific logging
113
+ logging_lambda = lambda do | message, type="info" |
114
+ if type == 'debug'
115
+ Jekyll.logger.debug "Pagination:","#{message}"
116
+ elsif type == 'error'
117
+ Jekyll.logger.error "Pagination:", "#{message}"
118
+ elsif type == 'warn'
119
+ Jekyll.logger.warn "Pagination:", "#{message}"
120
+ else
121
+ Jekyll.logger.info "Pagination:", "#{message}"
122
+ end
123
+ end
124
+
125
+ ################ 4 ####################
126
+ # Now create and call the model with the real-life page creation proc and site data
127
+ model = PaginationModel.new(logging_lambda, page_add_lambda, page_remove_lambda, collection_by_name_lambda)
128
+ if( default_config['legacy'] ) #(REMOVE AFTER 2018-01-01)
129
+ Jekyll.logger.warn "Pagination:", "You are running jekyll-paginate backwards compatible pagination logic. Please ignore all earlier warnings displayed related to the old jekyll-paginate gem."
130
+ all_posts = site.site_payload['site']['posts'].reject { |post| post['hidden'] }
131
+ model.run_compatability(default_config, all_pages, site_title, all_posts) #(REMOVE AFTER 2018-01-01)
132
+ else
133
+ count = model.run(default_config, all_pages, site_title)
134
+ Jekyll.logger.info ""
135
+ Jekyll.logger.info "Pagination:", "Complete, processed #{count} pagination page(s)"
136
+ end
137
+
138
+ #rescue => ex
139
+ # puts ex.backtrace
140
+ # raise
141
+ #end
142
+ end # function generate
143
+ end # class PaginationGenerator
144
+
145
+ end # module J1Paginator
146
+ end # module Jekyll
@@ -0,0 +1,116 @@
1
+ module Jekyll
2
+ module J1Paginator::Generator
3
+
4
+ #
5
+ # Performs indexing of the posts or collection documents
6
+ # as well as filtering said collections when requested by the defined filters.
7
+ class PaginationIndexer
8
+ #
9
+ # Create a hash index for all post based on a key in the post.data table
10
+ #
11
+ def self.index_posts_by(all_posts, index_key)
12
+ return nil if all_posts.nil?
13
+ return all_posts if index_key.nil?
14
+ index = {}
15
+ all_posts.each do |post|
16
+ next if post.data.nil?
17
+ next if !post.data.has_key?(index_key)
18
+ next if post.data[index_key].nil?
19
+ next if post.data[index_key].size <= 0
20
+ next if post.data[index_key].to_s.strip.length == 0
21
+
22
+ # Only tags and categories come as premade arrays, locale does not, so convert any data
23
+ # elements that are strings into arrays
24
+ post_data = post.data[index_key]
25
+ if post_data.is_a?(String)
26
+ post_data = post_data.split(/;|,|\s/)
27
+ end
28
+
29
+ post_data.each do |key|
30
+ key = key.to_s.downcase.strip
31
+ # If the key is a delimetered list of values
32
+ # (meaning the user didn't use an array but a string with commas)
33
+ key.split(/;|,/).each do |k_split|
34
+ k_split = k_split.to_s.downcase.strip #Clean whitespace and junk
35
+ if !index.has_key?(k_split)
36
+ index[k_split.to_s] = []
37
+ end
38
+ index[k_split.to_s] << post
39
+ end
40
+ end
41
+ end
42
+ return index
43
+ end # function index_posts_by
44
+
45
+ #
46
+ # Creates an intersection (only returns common elements)
47
+ # between multiple arrays
48
+ #
49
+ def self.intersect_arrays(first, *rest)
50
+ return nil if first.nil?
51
+ return nil if rest.nil?
52
+
53
+ intersect = first
54
+ rest.each do |item|
55
+ return [] if item.nil?
56
+ intersect = intersect & item
57
+ end
58
+ return intersect
59
+ end #function intersect_arrays
60
+
61
+ #
62
+ # Filters posts based on a keyed source_posts hash of indexed posts and performs a intersection of
63
+ # the two sets. Returns only posts that are common between all collections
64
+ #
65
+ def self.read_config_value_and_filter_posts(config, config_key, posts, source_posts)
66
+ return nil if posts.nil?
67
+ return nil if source_posts.nil? # If the source is empty then simply don't do anything
68
+ return posts if config.nil?
69
+
70
+ plural_key = Utils.plural(config_key)
71
+
72
+ return posts if !config.has_key?(config_key) && !config.has_key?(plural_key)
73
+ return posts if config[config_key].nil? && config[plural_key].nil?
74
+
75
+ # Get the filter values from the config (this is the cat/tag/locale values that should be filtered on)
76
+
77
+ if config[config_key].is_a?(Hash) || config[plural_key].is_a?(Hash)
78
+ # { values: [..], matching: any|all }
79
+ config_hash = config[config_key].is_a?(Hash) ? config[config_key] : config[plural_key]
80
+ config_value = Utils.config_values(config_hash, 'value')
81
+ matching = config_hash['matching'] || 'all'
82
+ else
83
+ # Default array syntax
84
+ config_value = Utils.config_values(config, config_key)
85
+ matching = 'all'
86
+ end
87
+
88
+ matching = matching.to_s.downcase.strip
89
+
90
+ # Filter on any/all specified categories, etc.
91
+
92
+ if matching == "all"
93
+ # Now for all filter values for the config key, let's remove all items from the posts that
94
+ # aren't common for all collections that the user wants to filter on
95
+ config_value.each do |key|
96
+ key = key.to_s.downcase.strip
97
+ posts = PaginationIndexer.intersect_arrays(posts, source_posts[key])
98
+ end
99
+
100
+ elsif matching == "any"
101
+ # "or" filter: Remove posts that don't have at least one required key
102
+ posts.delete_if { |post|
103
+ post_config = Utils.config_values(post.data, config_key)
104
+ (config_value & post_config).empty?
105
+ }
106
+
107
+ # else no filter
108
+ end
109
+
110
+ # The fully filtered final post list
111
+ return posts
112
+ end #function read_config_value_and_filter_posts
113
+ end #class PaginationIndexer
114
+
115
+ end #module J1Paginator
116
+ end #module Jekyll