jekyll_all_collections 0.3.6 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +19 -1
- data/CHANGELOG.md +34 -8
- data/README.md +208 -105
- data/jekyll_all_collections.gemspec +11 -14
- data/lib/hooks/a_page.rb +69 -0
- data/lib/hooks/all_collections_hooks.rb +59 -0
- data/lib/hooks/all_files.rb +46 -0
- data/lib/hooks/class_methods.rb +50 -0
- data/lib/jekyll_all_collections/version.rb +1 -1
- data/lib/jekyll_all_collections.rb +14 -2
- data/lib/tag/all_collections_tag.rb +158 -0
- data/lib/util/mslinn_binary_search.rb +152 -0
- data/lib/util/send_chain.rb +58 -0
- data/spec/all_collections_tag/all_collections_tag_sort_spec.rb +1 -0
- data/spec/bsearch_spec.rb +50 -0
- data/spec/date_sort_spec.rb +8 -6
- data/spec/mslinn_binary_search_spec.rb +47 -0
- data/spec/send_chain_spec.rb +72 -0
- data/spec/send_spec.rb +28 -0
- data/spec/sorted_lru_files_spec.rb +82 -0
- data/spec/spec_helper.rb +1 -3
- data/spec/status_persistence.txt +32 -22
- data/spec/testable_spec.rb +38 -0
- metadata +33 -11
- data/lib/all_collections_hooks.rb +0 -118
- data/lib/all_collections_tag.rb +0 -139
@@ -3,19 +3,14 @@ require_relative 'lib/jekyll_all_collections/version'
|
|
3
3
|
Gem::Specification.new do |spec|
|
4
4
|
github = 'https://github.com/mslinn/jekyll_all_collections'
|
5
5
|
|
6
|
-
spec.authors
|
7
|
-
spec.bindir = 'exe'
|
6
|
+
spec.authors = ['Mike Slinn']
|
8
7
|
spec.description = <<~END_OF_DESC
|
9
|
-
Provides
|
8
|
+
Provides normalized collections and extra functionality for Jekyll websites.
|
10
9
|
END_OF_DESC
|
11
|
-
spec.email
|
12
|
-
spec.
|
13
|
-
|
14
|
-
# Specify which files should be added to the gem when it is released.
|
15
|
-
spec.files = Dir['.rubocop.yml', 'LICENSE.*', 'Rakefile', '{lib,spec}/**/*', '*.gemspec', '*.md']
|
16
|
-
|
10
|
+
spec.email = ['mslinn@mslinn.com']
|
11
|
+
spec.files = Dir['.rubocop.yml', 'LICENSE.*', 'Rakefile', '{lib,spec}/**/*', '*.gemspec', '*.md']
|
17
12
|
spec.homepage = 'https://www.mslinn.com/jekyll_plugins/jekyll_all_collections.html'
|
18
|
-
spec.license
|
13
|
+
spec.license = 'MIT'
|
19
14
|
spec.metadata = {
|
20
15
|
'allowed_push_host' => 'https://rubygems.org',
|
21
16
|
'bug_tracker_uri' => "#{github}/issues",
|
@@ -23,13 +18,15 @@ Gem::Specification.new do |spec|
|
|
23
18
|
'homepage_uri' => spec.homepage,
|
24
19
|
'source_code_uri' => github,
|
25
20
|
}
|
26
|
-
spec.name
|
27
|
-
spec.
|
21
|
+
spec.name = 'jekyll_all_collections'
|
22
|
+
spec.platform = Gem::Platform::RUBY
|
23
|
+
spec.require_paths = ['lib']
|
28
24
|
spec.required_ruby_version = '>= 2.6.0'
|
29
|
-
spec.summary
|
30
|
-
spec.version
|
25
|
+
spec.summary = 'Provides normalized collections and extra functionality for Jekyll websites.'
|
26
|
+
spec.version = JekyllAllCollectionsVersion::VERSION
|
31
27
|
|
32
28
|
spec.add_dependency 'jekyll', '>= 3.5.0'
|
33
29
|
spec.add_dependency 'jekyll_draft'
|
34
30
|
spec.add_dependency 'jekyll_plugin_support'
|
31
|
+
spec.add_dependency 'sorted_set'
|
35
32
|
end
|
data/lib/hooks/a_page.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
module AllCollectionsHooks
|
2
|
+
class APage
|
3
|
+
attr_reader :content, :data, :date, :description, :destination, :draft, :excerpt, :ext, :extname, :href,
|
4
|
+
:label, :last_modified, :layout, :origin, :path, :relative_path, :tags, :title, :type, :url
|
5
|
+
|
6
|
+
def initialize(obj, origin)
|
7
|
+
@origin = origin
|
8
|
+
data_field_init obj
|
9
|
+
obj_field_init obj
|
10
|
+
@draft = Jekyll::Draft.draft? obj
|
11
|
+
@href = @url if @href.nil?
|
12
|
+
# @href = "/#{@href}" if @origin == 'individual_page'
|
13
|
+
@href = "#{@href}index.html" if @href.end_with? '/'
|
14
|
+
@name = File.basename(@href)
|
15
|
+
@title = if @data&.key?('title')
|
16
|
+
@data['title']
|
17
|
+
elsif obj.respond_to?(:title)
|
18
|
+
obj.title
|
19
|
+
else
|
20
|
+
"<code>#{@href}</code>"
|
21
|
+
end
|
22
|
+
rescue StandardError => e
|
23
|
+
JekyllSupport.error_short_trace(@logger, e)
|
24
|
+
# JekyllSupport.warn_short_trace(@logger, e)
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
@label || @date.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def data_field_init(obj)
|
34
|
+
return unless obj.respond_to? :data
|
35
|
+
|
36
|
+
@data = obj.data
|
37
|
+
|
38
|
+
@categories = @data['categories'] if @data.key? 'categories'
|
39
|
+
@date = (@data['date'].to_date if @data&.key?('date')) || Date.today
|
40
|
+
@description = @data['description'] if @data.key? 'description'
|
41
|
+
@excerpt = @data['excerpt'] if @data.key? 'excerpt'
|
42
|
+
@ext ||= @data['ext'] if @data.key? 'ext'
|
43
|
+
@last_modified = @data['last_modified'] || @data['last_modified_at'] || @date
|
44
|
+
@last_modified_field = case @data
|
45
|
+
when @data.key?('last_modified')
|
46
|
+
'last_modified'
|
47
|
+
when @data.key?('last_modified_at')
|
48
|
+
'last_modified_at'
|
49
|
+
end
|
50
|
+
@layout = @data['layout'] if @data.key? 'layout'
|
51
|
+
@tags = @data['tags'] if @data.key? 'tags'
|
52
|
+
end
|
53
|
+
|
54
|
+
def obj_field_init(obj)
|
55
|
+
@content = obj.content if obj.respond_to? :content
|
56
|
+
|
57
|
+
# TODO: What _config.yml setting should be passed to destination()?
|
58
|
+
@destination = obj.destination('') if obj.respond_to? :destination
|
59
|
+
@ext = obj.extname
|
60
|
+
@extname = @ext # For compatibility with previous versions of all_collections
|
61
|
+
@label = obj.collection.label if obj.respond_to?(:collection) && obj.collection.respond_to?(:label)
|
62
|
+
@path = obj.path if obj.respond_to? :path
|
63
|
+
@relative_path = obj.relative_path if obj.respond_to? :relative_path
|
64
|
+
@type = obj.type if obj.respond_to? :type
|
65
|
+
@url = obj.url
|
66
|
+
@url = "#{@url}index.html" if @url.end_with? '/'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module AllCollectionsHooks
|
2
|
+
class << self
|
3
|
+
attr_accessor :logger
|
4
|
+
end
|
5
|
+
@logger = PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config)
|
6
|
+
|
7
|
+
# No, all_collections is not defined for this hook
|
8
|
+
# Jekyll::Hooks.register(:site, :after_init, priority: :normal) do |site|
|
9
|
+
# defined_msg = AllCollectionsHooks.all_collections_defined?(site)
|
10
|
+
# @logger.debug { "Jekyll::Hooks.register(:site, :after_init: #{defined_msg}" }
|
11
|
+
# end
|
12
|
+
|
13
|
+
# Creates a `Array[APage]` property called site.all_collections if it does not already exist.
|
14
|
+
# The array is available from :site, :pre_render onwards
|
15
|
+
# Each `APage` entry is one document or page.
|
16
|
+
Jekyll::Hooks.register(:site, :post_read, priority: :normal) do |site|
|
17
|
+
@site = site
|
18
|
+
unless site.class.method_defined? :all_collections
|
19
|
+
defined_msg = AllCollectionsHooks.all_collections_defined? site
|
20
|
+
@logger.debug { "Jekyll::Hooks.register(:site, :post_read, :normal: #{defined_msg}" }
|
21
|
+
AllCollectionsHooks.compute(site) if !@site.class.method_defined?(:all_documents) || @site.all_documents.nil?
|
22
|
+
end
|
23
|
+
rescue StandardError => e
|
24
|
+
JekyllSupport.error_short_trace(@logger, e)
|
25
|
+
# JekyllSupport.warn_short_trace(@logger, e)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Yes, all_collections is defined for this hook
|
29
|
+
# Jekyll::Hooks.register(:site, :post_read, priority: :low) do |site|
|
30
|
+
# defined_msg = AllCollectionsHooks.all_collections_defined?(site)
|
31
|
+
# @logger.debug { "Jekyll::Hooks.register(:site, :post_read, :low: #{defined_msg}" }
|
32
|
+
# rescue StandardError => e
|
33
|
+
# JekyllSupport.error_short_trace(@logger, e)
|
34
|
+
# # JekyllSupport.warn_short_trace(@logger, e)
|
35
|
+
# end
|
36
|
+
|
37
|
+
# Yes, all_collections is defined for this hook
|
38
|
+
# Jekyll::Hooks.register(:site, :post_read, priority: :normal) do |site|
|
39
|
+
# defined_msg = AllCollectionsHooks.all_collections_defined?(site)
|
40
|
+
# @logger.debug { "Jekyll::Hooks.register(:site, :post_read, :normal: #{defined_msg}" }
|
41
|
+
# rescue StandardError => e
|
42
|
+
# JekyllSupport.error_short_trace(@logger, e)
|
43
|
+
# # JekyllSupport.warn_short_trace(@logger, e)
|
44
|
+
# end
|
45
|
+
|
46
|
+
# Yes, all_collections is defined for this hook
|
47
|
+
# Jekyll::Hooks.register(:site, :pre_render, priority: :normal) do |site, _payload|
|
48
|
+
# defined_msg = AllCollectionsHooks.all_collections_defined?(site)
|
49
|
+
# @logger.debug { "Jekyll::Hooks.register(:site, :pre_render: #{defined_msg}" }
|
50
|
+
# rescue StandardError => e
|
51
|
+
# JekyllSupport.error_short_trace(@logger, e)
|
52
|
+
# # JekyllSupport.warn_short_trace(@logger, e)
|
53
|
+
# end
|
54
|
+
end
|
55
|
+
|
56
|
+
PluginMetaLogger.instance.logger.info do
|
57
|
+
"Loaded AllCollectionsHooks v#{JekyllAllCollectionsVersion::VERSION} :site, :pre_render, :normal hook plugin."
|
58
|
+
end
|
59
|
+
Liquid::Template.register_filter(AllCollectionsHooks)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative '../util/send_chain'
|
2
|
+
|
3
|
+
# Insert the url of a Jekyll::Page into each LruFile instance,
|
4
|
+
# along with the Page reference
|
5
|
+
# todo: replace references to url and :url with reverse_url and :reverse_url
|
6
|
+
LruFile = Struct.new(:url, :page) do
|
7
|
+
include SendChain
|
8
|
+
|
9
|
+
def <=>(other)
|
10
|
+
url <=> other.url
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Matches suffixes of an array of urls
|
15
|
+
# Converts suffixes to prefixes
|
16
|
+
class SortedLruFiles
|
17
|
+
attr_reader :msbs
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@msbs = MSlinnBinarySearch.new %i[url start_with?]
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param apages [Array[APage]]
|
24
|
+
def add_pages(apages)
|
25
|
+
apages.each { |apage| insert apage.href, apage }
|
26
|
+
@msbs.enable_search
|
27
|
+
end
|
28
|
+
|
29
|
+
def enable_search
|
30
|
+
@msbs.enable_search
|
31
|
+
end
|
32
|
+
|
33
|
+
def find(suffix)
|
34
|
+
@msbs.find suffix
|
35
|
+
end
|
36
|
+
|
37
|
+
def insert(url, file)
|
38
|
+
lru_file = LruFile.new(url.reverse, file)
|
39
|
+
lru_file.new_chain [:url, %i[start_with? placeholder]]
|
40
|
+
@msbs.insert(lru_file)
|
41
|
+
end
|
42
|
+
|
43
|
+
def select(suffix)
|
44
|
+
@msbs.select_pages suffix
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module AllCollectionsHooks
|
2
|
+
class << self
|
3
|
+
attr_accessor :all_collections, :all_documents, :everything, :sorted_lru_files
|
4
|
+
end
|
5
|
+
|
6
|
+
# @sort_by = ->(apages, criteria) { [apages.sort(criteria)] } # todo delete this
|
7
|
+
|
8
|
+
# @return [String] indicating if :all_collections is defined or not
|
9
|
+
def self.all_collections_defined?(site)
|
10
|
+
"site.all_collections #{site.class.method_defined?(:all_collections) ? 'IS' : 'IS NOT'} defined"
|
11
|
+
end
|
12
|
+
|
13
|
+
# Create Array of AllCollectionsHooks::APage from objects
|
14
|
+
# @param objects [Array] An array of Jekyll::Document, Jekyll::Page or file names
|
15
|
+
# @param origin [String] Indicates type of objects being passed
|
16
|
+
def self.apages_from_objects(objects, origin)
|
17
|
+
pages = []
|
18
|
+
objects.each do |object|
|
19
|
+
page = APage.new(object, origin)
|
20
|
+
pages << page unless page.data['exclude_from_all'] || page.path == 'redirect.html'
|
21
|
+
end
|
22
|
+
pages
|
23
|
+
end
|
24
|
+
|
25
|
+
# Called by early, high-priority hook.
|
26
|
+
# Computes site.all_collections, site.all_documents, site.everything, and site.sorted_lru_files
|
27
|
+
def self.compute(site)
|
28
|
+
site.class.module_eval { attr_accessor :all_collections, :all_documents, :everything, :sorted_lru_files }
|
29
|
+
|
30
|
+
documents = site.collections
|
31
|
+
.values
|
32
|
+
.map { |x| x.class.method_defined?(:docs) ? x.docs : x }
|
33
|
+
.flatten
|
34
|
+
.compact
|
35
|
+
@all_collections = AllCollectionsHooks.apages_from_objects(documents, 'collection')
|
36
|
+
@all_documents = @all_collections +
|
37
|
+
AllCollectionsHooks.apages_from_objects(site.pages, 'individual_page')
|
38
|
+
@everything = @all_documents +
|
39
|
+
AllCollectionsHooks.apages_from_objects(site.static_files, 'static_file')
|
40
|
+
@sorted_lru_files = SortedLruFiles.new.add_pages @everything
|
41
|
+
|
42
|
+
site.all_collections = @all_collections
|
43
|
+
site.all_documents = @all_documents
|
44
|
+
site.everything = @everything
|
45
|
+
site.sorted_lru_files = @sorted_lru_files
|
46
|
+
rescue StandardError => e
|
47
|
+
JekyllSupport.error_short_trace(AllCollectionsHooks.logger, e)
|
48
|
+
# JekyllSupport.warn_short_trace(AllCollectionsHooks.logger, e)
|
49
|
+
end
|
50
|
+
end
|
@@ -1,5 +1,17 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
def require_directory(dir)
|
2
|
+
Dir[File.join(dir, '*.rb')]&.sort&.each do |file|
|
3
|
+
require file unless file == __FILE__
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'jekyll'
|
8
|
+
require 'jekyll_plugin_logger'
|
9
|
+
require 'jekyll_plugin_support'
|
10
|
+
|
11
|
+
require_relative 'jekyll_all_collections/version'
|
12
|
+
require_directory "#{__dir__}/hooks"
|
13
|
+
require_directory "#{__dir__}/tag"
|
14
|
+
require_directory "#{__dir__}/util"
|
3
15
|
|
4
16
|
module JekyllAllCollections
|
5
17
|
include AllCollectionsHooks
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'jekyll_draft'
|
2
|
+
require 'jekyll_plugin_support'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
# @author Copyright 2020 Michael Slinn
|
6
|
+
# @license SPDX-License-Identifier: Apache-2.0
|
7
|
+
module AllCollectionsTag
|
8
|
+
PLUGIN_NAME = 'all_collections'.freeze
|
9
|
+
CRITERIA = %w[date destination draft label last_modified last_modified_at path relative_path title type url].freeze
|
10
|
+
DRAFT_HTML = '<i class="jekyll_draft">Draft</i>'.freeze
|
11
|
+
|
12
|
+
class AllCollectionsTag < JekyllSupport::JekyllTag
|
13
|
+
include JekyllAllCollectionsVersion
|
14
|
+
|
15
|
+
# Method prescribed by JekyllTag.
|
16
|
+
# @return [String]
|
17
|
+
def render_impl
|
18
|
+
parse_arguments # Defines instance variables like @sort_by
|
19
|
+
sort_lambda = init_sort_by @sort_by, @sort_by_param
|
20
|
+
@heading = @helper.parameter_specified?('heading') || default_head(@sort_by)
|
21
|
+
generate_output sort_lambda
|
22
|
+
rescue StandardError => e
|
23
|
+
JekyllSupport.error_short_trace @logger, e
|
24
|
+
# JekyllSupport.warn_short_trace @logger, e
|
25
|
+
end
|
26
|
+
|
27
|
+
# Descending sort keys reverse the order of comparison
|
28
|
+
# Example return values:
|
29
|
+
# "->(a, b) { [a.last_modified] <=> [b.last_modified] }"
|
30
|
+
# "->(a, b) { [b.last_modified] <=> [a.last_modified] }" (descending)
|
31
|
+
# "->(a, b) { [a.last_modified, a.date] <=> [b.last_modified, b.date] }" (descending last_modified, ascending date)
|
32
|
+
# "->(a, b) { [a.last_modified, b.date] <=> [b.last_modified, a.date] }" (ascending last_modified, descending date)
|
33
|
+
def self.create_lambda_string(criteria)
|
34
|
+
criteria_lhs_array = []
|
35
|
+
criteria_rhs_array = []
|
36
|
+
verify_sort_by_type(criteria).each do |c|
|
37
|
+
descending_sort = c.start_with? '-'
|
38
|
+
c.delete_prefix! '-'
|
39
|
+
abort("Error: '#{c}' is not a valid sort field. Valid field names are: #{CRITERIA.join ', '}") \
|
40
|
+
unless CRITERIA.include?(c)
|
41
|
+
criteria_lhs_array << (descending_sort ? "b.#{c}" : "a.#{c}")
|
42
|
+
criteria_rhs_array << (descending_sort ? "a.#{c}" : "b.#{c}")
|
43
|
+
end
|
44
|
+
"->(a, b) { [#{criteria_lhs_array.join(', ')}] <=> [#{criteria_rhs_array.join(', ')}] }"
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.verify_sort_by_type(sort_by)
|
48
|
+
case sort_by
|
49
|
+
when Array
|
50
|
+
sort_by
|
51
|
+
when Enumerable
|
52
|
+
sort_by.to_a
|
53
|
+
when Date
|
54
|
+
[sort_by.to_i]
|
55
|
+
when String
|
56
|
+
[sort_by]
|
57
|
+
else
|
58
|
+
abort "Error: @sort_by was specified as '#{sort_by}'; it must either be a string or an array of strings"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def default_head(sort_by)
|
65
|
+
criteria = (sort_by.map do |x|
|
66
|
+
reverse = x.start_with? '-'
|
67
|
+
criterion = x.delete_prefix('-').capitalize
|
68
|
+
criterion += reverse ? ' (Newest to Oldest)' : ' (Oldest to Newest)'
|
69
|
+
criterion
|
70
|
+
end).join(', ')
|
71
|
+
"All Posts in All Categories Sorted By #{criteria}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def evaluate(string)
|
75
|
+
self.eval string, binding, __FILE__, __LINE__
|
76
|
+
rescue StandardError => e
|
77
|
+
warn_short_trace e.red
|
78
|
+
end
|
79
|
+
|
80
|
+
def last_modified_value(apage)
|
81
|
+
@logger.debug do
|
82
|
+
" apage.last_modified='#{apage.last_modified}'; " \
|
83
|
+
"apage.last_modified_at='#{apage.last_modified_at}'; " \
|
84
|
+
"@date_column='#{@date_column}'"
|
85
|
+
end
|
86
|
+
last_modified = if @date_column == 'last_modified' && apage.respond_to?(:last_modified)
|
87
|
+
apage.last_modified
|
88
|
+
elsif apage.respond_to? :last_modified_at
|
89
|
+
apage.last_modified_at
|
90
|
+
else
|
91
|
+
apage.date
|
92
|
+
end
|
93
|
+
last_modified ||= apage.date || Date.today
|
94
|
+
last_modified
|
95
|
+
end
|
96
|
+
|
97
|
+
def generate_output(sort_lambda)
|
98
|
+
id = @id.to_s.strip.empty? ? '' : " id='#{@id}'"
|
99
|
+
heading = @heading.strip.to_s.empty? ? '' : "<h2#{id}>#{@heading}</h2>"
|
100
|
+
data = case @data_selector
|
101
|
+
when 'all_collections'
|
102
|
+
@site.all_collections
|
103
|
+
when 'all_documents'
|
104
|
+
@site.all_documents
|
105
|
+
when 'everything'
|
106
|
+
@site.everything
|
107
|
+
else
|
108
|
+
raise AllCollectionsError, "Invalid value for @data_selector (#{data_selector})"
|
109
|
+
end
|
110
|
+
collection = data.sort(&sort_lambda)
|
111
|
+
posts = collection.map do |x|
|
112
|
+
last_modified = last_modified_value x
|
113
|
+
date = last_modified.strftime '%Y-%m-%d'
|
114
|
+
draft = x.draft ? DRAFT_HTML : ''
|
115
|
+
href = "<a href='#{x.href}'>#{x.title}</a>"
|
116
|
+
@logger.debug { " date='#{date}' #{x.title}\n" }
|
117
|
+
" <span>#{date}</span><span>#{href}#{draft}</span>"
|
118
|
+
end
|
119
|
+
<<~END_TEXT
|
120
|
+
#{heading}
|
121
|
+
<div class="posts">
|
122
|
+
#{posts.join "\n"}
|
123
|
+
</div>
|
124
|
+
END_TEXT
|
125
|
+
rescue ArgumentError => e
|
126
|
+
warn_short_trace e
|
127
|
+
end
|
128
|
+
|
129
|
+
# See https://stackoverflow.com/a/75377832/553865
|
130
|
+
def init_sort_by(sort_by, sort_by_param)
|
131
|
+
sort_lambda_string = AllCollectionsTag.create_lambda_string sort_by
|
132
|
+
|
133
|
+
@logger.debug do
|
134
|
+
"#{@page['path']} sort_by_param=#{sort_by_param} " \
|
135
|
+
"sort_lambda_string = #{sort_lambda_string}\n"
|
136
|
+
end
|
137
|
+
|
138
|
+
evaluate sort_lambda_string
|
139
|
+
end
|
140
|
+
|
141
|
+
def parse_arguments
|
142
|
+
@data_selector = @helper.parameter_specified?('data_selector') || 'all_collections'
|
143
|
+
abort "Invalid data_selector #{@data_selector}" unless %w[all_collections all_documents everything].include? @data_selector
|
144
|
+
|
145
|
+
@date_column = @helper.parameter_specified?('date_column') || 'date'
|
146
|
+
unless %w[date last_modified].include?(@date_column)
|
147
|
+
raise AllCollectionsError "The date_column attribute must either have value 'date' or 'last_modified', " \
|
148
|
+
"but '#{@date_column}' was specified"
|
149
|
+
end
|
150
|
+
|
151
|
+
@id = @helper.parameter_specified?('id') || SecureRandom.hex(10)
|
152
|
+
@sort_by_param = @helper.parameter_specified? 'sort_by'
|
153
|
+
@sort_by = (@sort_by_param&.delete(' ')&.split(',') if @sort_by_param != false) || ['-date']
|
154
|
+
end
|
155
|
+
|
156
|
+
JekyllSupport::JekyllPluginHelper.register(self, PLUGIN_NAME)
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
unless defined?(MSlinnBinarySearchError)
|
2
|
+
class MSlinnBinarySearchError < StandardError
|
3
|
+
end
|
4
|
+
end
|
5
|
+
|
6
|
+
# Ruby's binary search is unsuitable because the value to be searched for changes the required ordering for String compares
|
7
|
+
class MSlinnBinarySearch
|
8
|
+
attr_reader :accessor_chain, :array # For testing only
|
9
|
+
|
10
|
+
def initialize(accessor_chain)
|
11
|
+
@array = SortedSet.new # [LruFile] Ordered highest to lowest
|
12
|
+
@accessor_chain = accessor_chain
|
13
|
+
end
|
14
|
+
|
15
|
+
# Convert the SortedSet to an Array
|
16
|
+
def enable_search
|
17
|
+
@array = @array.to_a
|
18
|
+
end
|
19
|
+
|
20
|
+
# A match is found when the Array[LruFile] has an href which starts with the given stem
|
21
|
+
# @param stem [String]
|
22
|
+
# @return first item from @array.url that matches, or nil if no match
|
23
|
+
def find(stem)
|
24
|
+
raise MSlinnBinarySearchError, 'Invalid find because stem to search for is nil.' if stem.nil?
|
25
|
+
|
26
|
+
index = find_index(stem)
|
27
|
+
return nil if index.nil?
|
28
|
+
|
29
|
+
@array[index]
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param stem [String]
|
33
|
+
# @return index of first matching stem, or nil if @array is empty, or 0 if no stem specified
|
34
|
+
def find_index(stem)
|
35
|
+
return nil if @array.empty?
|
36
|
+
return 0 if stem.nil? || stem.empty?
|
37
|
+
|
38
|
+
mets = stem.reverse
|
39
|
+
return nil if @array[0].url[0...mets.size] > mets # TODO: use chain eval for item
|
40
|
+
return nil if @array[0].url[0] != mets[0]
|
41
|
+
|
42
|
+
_find_index(mets, 0, @array.length - 1)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @param stem [String]
|
46
|
+
# @return [index] of matching values, or [] if @array is empty, or entire array if no stem specified
|
47
|
+
def find_indices(stem)
|
48
|
+
return [] if @array.empty?
|
49
|
+
return @array if stem.nil? || stem.empty?
|
50
|
+
|
51
|
+
first_index = _find_index(stem, 0, @array.length - 1)
|
52
|
+
last_index = first_index
|
53
|
+
last_index += 1 while @array[last_index].url.start_with? stem
|
54
|
+
[first_index..last_index]
|
55
|
+
end
|
56
|
+
|
57
|
+
# @param item [LruFile]
|
58
|
+
# @return [int] index of matching LruFile in @array, or nil if not found
|
59
|
+
def index_of(lru_file)
|
60
|
+
raise MSlinnBinarySearchError, 'Invalid index_of lru_file (nil).' if lru_file.nil?
|
61
|
+
|
62
|
+
find_index lru_file.url
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [LruFile] item at given index in @array
|
66
|
+
def item_at(index)
|
67
|
+
if index > @array.length - 1
|
68
|
+
raise MSlinnBinarySearchError,
|
69
|
+
"Invalid item_at index (#{index}) is greater than maximum stem (#{@array.length - 1})."
|
70
|
+
end
|
71
|
+
raise MSlinnBinarySearchError, "Invalid item_at index (#{index}) is less than zero." if index.negative?
|
72
|
+
|
73
|
+
@array[index]
|
74
|
+
end
|
75
|
+
|
76
|
+
# @param lru_file [LruFile]
|
77
|
+
def insert(lru_file)
|
78
|
+
raise MSlinnBinarySearchError, 'Invalid insert because new item is nil.' if lru_file.nil?
|
79
|
+
raise MSlinnBinarySearchError, "Invalid insert because new item has no chain (#{lru_file})" if lru_file.chain.nil?
|
80
|
+
|
81
|
+
@array.add lru_file
|
82
|
+
end
|
83
|
+
|
84
|
+
# TODO: Cache this method
|
85
|
+
# @param suffix [String] to use stem search on
|
86
|
+
# @return nil if @array is empty
|
87
|
+
# @return the first item in @array if suffix is nil or an empty string
|
88
|
+
def prefix_search(suffix)
|
89
|
+
return nil if @array.empty?
|
90
|
+
return @array[0] if suffix.empty? || suffix.nil?
|
91
|
+
|
92
|
+
low = search_index { |x| x.evaluate_with suffix }
|
93
|
+
return [] if low.nil?
|
94
|
+
|
95
|
+
high = low
|
96
|
+
high += 1 while high < @array.length &&
|
97
|
+
@array[high].evaluate_with(suffix)
|
98
|
+
@array[low..high]
|
99
|
+
end
|
100
|
+
|
101
|
+
# @param stem [String]
|
102
|
+
# @return [APage] matching APages, or [] if @array is empty, or entire array if no stem specified
|
103
|
+
def select_pages(stem)
|
104
|
+
first_index = find_index stem
|
105
|
+
return [] if first_index.nil?
|
106
|
+
|
107
|
+
last_index = first_index
|
108
|
+
while last_index < @array.length - 1
|
109
|
+
# LruFile.url is reversed, bug LruFile.page is not
|
110
|
+
break unless @array[last_index + 1].url.start_with?(stem.reverse)
|
111
|
+
|
112
|
+
last_index += 1
|
113
|
+
end
|
114
|
+
Range.new(first_index, last_index).map { |i| @array[i].page }
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
# A match is found when the Array[LruFile] has an href which starts with the given stem
|
120
|
+
# @param stem [String]
|
121
|
+
# @return [int] first index in @array that matches, or nil if no match
|
122
|
+
def _find_index(mets, min_index, max_index)
|
123
|
+
raise MSlinnBinarySearchError, "_find_index min_index(#{min_index})<0" if min_index.negative?
|
124
|
+
raise MSlinnBinarySearchError, "_find_index min_index(#{min_index})>max_index(#{max_index})" if min_index > max_index
|
125
|
+
raise MSlinnBinarySearchError, "_find_index max_index(#{max_index})>=@array.length(#{@array.length})" if max_index >= @array.length
|
126
|
+
|
127
|
+
return min_index if (min_index == max_index) && @array[min_index].url.start_with?(mets)
|
128
|
+
|
129
|
+
while min_index < max_index
|
130
|
+
mid_index = (min_index + max_index) / 2
|
131
|
+
mid_value = @array[mid_index].url[0...(mets.size)] # TODO: use chain eval for item
|
132
|
+
|
133
|
+
if mid_value == mets # back up until the first match is found
|
134
|
+
index = mid_index
|
135
|
+
loop do
|
136
|
+
return 0 if index.zero?
|
137
|
+
|
138
|
+
return index unless @array[index - 1].url.start_with?(mets)
|
139
|
+
|
140
|
+
index -= 1
|
141
|
+
end
|
142
|
+
elsif mid_value > mets
|
143
|
+
max_index = mid_index - 1
|
144
|
+
return _find_index(mets, min_index, max_index)
|
145
|
+
else
|
146
|
+
min_index = mid_index + 1
|
147
|
+
return _find_index(mets, min_index, max_index)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require_relative '../util/mslinn_binary_search'
|
2
|
+
|
3
|
+
# Supports one chain at a time
|
4
|
+
module SendChain
|
5
|
+
# See https://stackoverflow.com/a/79333706/553865
|
6
|
+
# This method can be called directly if no methods in the chain require arguments
|
7
|
+
# Does not use any external state
|
8
|
+
def send_chain(chain)
|
9
|
+
Array(chain).inject(self) { |o, a| o.send(*a) }
|
10
|
+
end
|
11
|
+
|
12
|
+
# Saves @chain structure containing :placeholders for arguments to be supplied later
|
13
|
+
# Call when a different chain with :placeholders is desired
|
14
|
+
def new_chain(chain)
|
15
|
+
abort "new_chain error: chain must be an array ('#{chain}' was an #{chain.class.name})" \
|
16
|
+
unless chain.instance_of?(Array)
|
17
|
+
@chain = chain
|
18
|
+
end
|
19
|
+
|
20
|
+
# Call after new_chain, to evaluate @chain with values
|
21
|
+
def substitute_and_send_chain_with(values)
|
22
|
+
send_chain substitute_chain_with values
|
23
|
+
end
|
24
|
+
|
25
|
+
alias evaluate_with substitute_and_send_chain_with
|
26
|
+
|
27
|
+
# Call this method after calling new_chain to perform error checking and replace :placeholders with values.
|
28
|
+
# @chain is not modified.
|
29
|
+
# @return [Array] Modified chain
|
30
|
+
def substitute_chain_with(values)
|
31
|
+
values = [values] unless values.instance_of?(Array)
|
32
|
+
|
33
|
+
placeholder_count = @chain.flatten.count { |x| x == :placeholder }
|
34
|
+
if values.length != placeholder_count
|
35
|
+
abort "with_values error: number of values (#{values.length}) does not match the number of placeholders (#{placeholder_count})"
|
36
|
+
end
|
37
|
+
|
38
|
+
eval_chain @chain, values
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# Replaces :placeholders with values
|
44
|
+
# Does not use any external state
|
45
|
+
# @return modified chain
|
46
|
+
def eval_chain(chain, values)
|
47
|
+
chain.map do |c|
|
48
|
+
case c
|
49
|
+
when :placeholder
|
50
|
+
values.shift
|
51
|
+
when Array
|
52
|
+
eval_chain c, values
|
53
|
+
else
|
54
|
+
c
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|