j1_paginator 2019.1.0

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