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,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,134 @@
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
+
23
+ # Retrieve and merge the pagination configuration from the
24
+ # plugin yml file
25
+ pg_config_defaults = site.data['plugins']['defaults']['paginator']
26
+ pg_config_settings = site.data['plugins']['paginator']
27
+ pg_settings = Jekyll::Utils.deep_merge_hashes(pg_config_defaults, pg_config_settings || {})
28
+
29
+ # Merge the pagination configuration by the site default settings
30
+ # default_config = Jekyll::Utils.deep_merge_hashes(DEFAULT, site.config['pagination'] || {})#
31
+ default_config = Jekyll::Utils.deep_merge_hashes(DEFAULT, pg_settings['settings']['pagination'] || {})
32
+ # default_config = pg_settings
33
+
34
+ # Compatibility Note: legacy paginate logic NOT supported by
35
+ # J1 Paginator. If the legacy paginate logic is configured then
36
+ # raise error
37
+ if !site.config['paginate'].nil?
38
+ Jekyll.logger.info "J1 Paginator:","legacy paginate configuration settings detected"
39
+ err_msg = "J1 Paginator does NOT support the old jekyll-paginate logic. Please disable legacy 'paginate:' config settings"
40
+ Jekyll.logger.error err_msg
41
+ raise ArgumentError.new(err_msg)
42
+ end # Compatibility END (REMOVE AFTER 2018-01-01)
43
+
44
+ # If disabled then simply quit
45
+ if !default_config['enabled']
46
+ Jekyll.logger.info "J1 Paginator:","plugin disabled"
47
+ return
48
+ end
49
+
50
+ # Generate the AutoPages first
51
+ J1Paginator::AutoPages.create_autopages(site)
52
+
53
+ # Handle deprecation of settings and features
54
+ if( !default_config['title_suffix' ].nil? )
55
+ Jekyll::Deprecator.deprecation_message "J1 Paginator: The 'title_suffix' configuration has been deprecated. Please use 'title'. See https://github.com/sverrirs/j1-paginator/blob/master/README-GENERATOR.md#site-configuration"
56
+ end
57
+
58
+ Jekyll.logger.info "J1 Paginator:","pagination enabled, start processing ..."
59
+
60
+ ################ 0 ####################
61
+ # Get all pages in the site (this will be used to find the pagination templates)
62
+ all_pages = site.pages
63
+
64
+ # Get the default title of the site (used as backup when there is no title available for pagination)
65
+ site_title = site.config['title']
66
+
67
+ # lambda (callback functions)
68
+ # ----------------------------------------------------------------------
69
+
70
+ # Specify the callback function that returns the correct docs/posts
71
+ # based on the collection name "posts" are just another collection in
72
+ # Jekyll but a specialized version that require timestamps
73
+ # This collection is the default and if the user doesn't specify a
74
+ # collection in their front-matter then that is the one we load
75
+ # If the collection is not found then empty array is returned
76
+ collection_by_name_lambda = lambda do |collection_name|
77
+ coll = []
78
+ if collection_name == "all"
79
+ # the 'all' collection_name is a special case and includes all collections in the site (except posts!!)
80
+ # this is useful when you want to list items across multiple collections
81
+ site.collections.each do |coll_name, coll_data|
82
+ if( !coll_data.nil? && coll_name != 'posts')
83
+ coll += coll_data.docs.select { |doc| !doc.data.has_key?('pagination') } # Exclude all pagination pages
84
+ end
85
+ end
86
+ else
87
+ # Just the one collection requested
88
+ if !site.collections.has_key?(collection_name)
89
+ return []
90
+ end
91
+
92
+ coll = site.collections[collection_name].docs.select { |doc| !doc.data.has_key?('pagination') } # Exclude all pagination pages
93
+ end
94
+ return coll
95
+ end
96
+
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
+ # Add the page to the site so that it is generated correctly
101
+ site.pages << newpage
102
+ return newpage # Return the site to the calling code
103
+ end
104
+
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
+ # Create a proc that will delegate logging
111
+ # Decoupling Jekyll specific logging
112
+ logging_lambda = lambda do | message, type="info" |
113
+ if type == 'debug'
114
+ Jekyll.logger.debug "J1 Paginator:","#{message}"
115
+ elsif type == 'error'
116
+ Jekyll.logger.error "J1 Paginator:", "#{message}"
117
+ elsif type == 'warn'
118
+ Jekyll.logger.warn "J1 Paginator:", "#{message}"
119
+ else
120
+ Jekyll.logger.info "J1 Paginator:", "#{message}"
121
+ end
122
+ end
123
+
124
+ # Create and call the model with the real-life page creation
125
+ # proc and site data
126
+ model = PaginationModel.new(logging_lambda, page_add_lambda, page_remove_lambda, collection_by_name_lambda)
127
+ count = model.run(default_config, all_pages, site_title)
128
+ Jekyll.logger.info "J1 Paginator:", "finished, processed #{count} pagination page|s"
129
+
130
+ end # function generate
131
+ end # class PaginationGenerator
132
+
133
+ end # module J1Paginator
134
+ 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