jekyll_plugin_support 1.1.0 → 3.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +38 -3
- data/CHANGELOG.md +30 -6
- data/README.md +474 -4
- data/jekyll_plugin_support.gemspec +3 -1
- data/lib/block/jekyll_plugin_support_block.rb +5 -6
- data/lib/block/jekyll_plugin_support_block_noarg.rb +1 -3
- data/lib/error/jekyll_custom_error.rb +6 -5
- data/lib/generator/jekyll_plugin_support_generator.rb +1 -7
- data/lib/helper/jekyll_plugin_helper.rb +5 -5
- data/lib/helper/jekyll_plugin_helper_class.rb +2 -2
- data/lib/hooks/a_page.rb +203 -0
- data/lib/hooks/all_collections_hooks.rb +61 -0
- data/lib/hooks/all_files.rb +48 -0
- data/lib/hooks/class_methods.rb +38 -0
- data/lib/jekyll_all_collections/all_collections_tag.rb +174 -0
- data/lib/jekyll_plugin_support/jekyll_plugin_support_class.rb +6 -7
- data/lib/jekyll_plugin_support/jekyll_plugin_support_spec_support.rb +1 -3
- data/lib/jekyll_plugin_support/version.rb +1 -1
- data/lib/jekyll_plugin_support.rb +27 -12
- data/lib/tag/jekyll_plugin_support_tag.rb +1 -4
- data/lib/tag/jekyll_plugin_support_tag_noarg.rb +0 -2
- data/lib/util/mslinn_binary_search.rb +152 -0
- data/lib/util/send_chain.rb +56 -0
- data/spec/all_collections_tag/all_collections_tag_sort_spec.rb +184 -0
- data/spec/bsearch_spec.rb +50 -0
- data/spec/custom_error_spec.rb +12 -10
- data/spec/date_sort_spec.rb +84 -0
- data/spec/jekyll_plugin_helper_options_spec.rb +9 -3
- data/spec/liquid_variable_parsing_spec.rb +7 -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 +15 -3
- data/spec/status_persistence.txt +4 -7
- data/spec/testable_spec.rb +38 -0
- metadata +56 -5
@@ -1,5 +1,3 @@
|
|
1
|
-
require_relative '../error/jekyll_plugin_error_handling'
|
2
|
-
|
3
1
|
module JekyllSupport
|
4
2
|
# Base class for Jekyll block tags
|
5
3
|
class JekyllBlock < Liquid::Block
|
@@ -35,7 +33,7 @@ module JekyllSupport
|
|
35
33
|
@helper = JekyllPluginHelper.new tag_name, markup, @logger, respond_to?(:no_arg_parsing)
|
36
34
|
|
37
35
|
@error_name = "#{tag_name.camelcase(:upper)}Error"
|
38
|
-
JekyllSupport::CustomError.factory @error_name
|
36
|
+
::JekyllSupport::CustomError.factory @error_name
|
39
37
|
end
|
40
38
|
|
41
39
|
# Liquid::Block subclasses do not render if there is no content within the tag
|
@@ -47,8 +45,8 @@ module JekyllSupport
|
|
47
45
|
# Method prescribed by the Jekyll plugin lifecycle.
|
48
46
|
# Defines @config, @envs, @mode, @page and @site
|
49
47
|
# @return [String]
|
50
|
-
def render(liquid_context)
|
51
|
-
@helper.liquid_context = JekyllSupport.inject_config_vars liquid_context # modifies liquid_context
|
48
|
+
def render(liquid_context) # rubocop: disable Metrics/AbcSize
|
49
|
+
@helper.liquid_context = ::JekyllSupport.inject_config_vars liquid_context # modifies liquid_context
|
52
50
|
text = super # Liquid variable values in content are looked up and substituted
|
53
51
|
|
54
52
|
@envs = liquid_context.environments.first
|
@@ -84,10 +82,11 @@ module JekyllSupport
|
|
84
82
|
render_impl(text)
|
85
83
|
rescue StandardError => e
|
86
84
|
e.shorten_backtrace
|
85
|
+
@logger.error e.full_message
|
87
86
|
file_name = e.backtrace[0]&.split(':')&.first
|
88
87
|
in_file_name = "in '#{file_name}' " if file_name
|
89
88
|
of_page = "of '#{@page['path']}'" if @page
|
90
|
-
@logger.error { "
|
89
|
+
@logger.error { "While processing line #{@line_number} #{of_page} for #{tag_name} #{in_file_name}- #{e.message}" }
|
91
90
|
binding.pry if @pry_on_standard_error # rubocop:disable Lint/Debugger
|
92
91
|
raise e if @die_on_standard_error
|
93
92
|
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require_relative '../error/jekyll_plugin_error_handling'
|
2
|
-
|
3
1
|
module JekyllSupport
|
4
2
|
class JekyllBlockNoArgParsing < JekyllBlock
|
5
3
|
attr_reader :argument_string, :helper, :line_number, :logger, :page, :site
|
@@ -14,7 +12,7 @@ module JekyllSupport
|
|
14
12
|
rescue StandardError => e
|
15
13
|
e.shorten_backtrace
|
16
14
|
@logger.error { e.full_message }
|
17
|
-
JekyllSupport.error_short_trace(@logger, e)
|
15
|
+
::JekyllSupport.error_short_trace(@logger, e)
|
18
16
|
end
|
19
17
|
|
20
18
|
# Liquid::Block subclasses do not render if there is no content within the tag
|
@@ -42,11 +42,12 @@ module JekyllSupport
|
|
42
42
|
END_MSG
|
43
43
|
end
|
44
44
|
|
45
|
-
def shorten_backtrace(backtrace_element_count =
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
45
|
+
def shorten_backtrace(backtrace_element_count = 6)
|
46
|
+
set_backtrace backtrace[0..backtrace_element_count]
|
47
|
+
# b = backtrace[0..backtrace_element_count - 1].map do |x|
|
48
|
+
# x.gsub(Dir.pwd + '/', './')
|
49
|
+
# end
|
50
|
+
# set_backtrace b
|
50
51
|
end
|
51
52
|
end
|
52
53
|
end
|
@@ -1,6 +1,3 @@
|
|
1
|
-
require 'jekyll'
|
2
|
-
require_relative '../error/jekyll_plugin_error_handling'
|
3
|
-
|
4
1
|
module JekyllSupport
|
5
2
|
# Base class for Jekyll generators.
|
6
3
|
# PluginMetaLogger.instance.config is populated with the contents of `_config.yml` before Jekyll::Generator instances run.
|
@@ -14,7 +11,6 @@ module JekyllSupport
|
|
14
11
|
@logger ||= PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config)
|
15
12
|
|
16
13
|
@error_name = "#{self.class.name}Error"
|
17
|
-
# JekyllSupport::CustomError.factory @error_name
|
18
14
|
|
19
15
|
@site = site
|
20
16
|
@config = @site.config
|
@@ -23,8 +19,6 @@ module JekyllSupport
|
|
23
19
|
|
24
20
|
@mode = ENV['JEKYLL_ENV'] || 'development'
|
25
21
|
|
26
|
-
# set_error_context(self.class)
|
27
|
-
|
28
22
|
generate_impl
|
29
23
|
rescue StandardError => e
|
30
24
|
e.shorten_backtrace
|
@@ -62,7 +56,7 @@ module JekyllSupport
|
|
62
56
|
PluginMetaLogger.instance.info { msg }
|
63
57
|
end
|
64
58
|
|
65
|
-
def set_error_context(klass)
|
59
|
+
def set_error_context(klass) # rubocop:disable Naming/AccessorMethodName
|
66
60
|
return unless Object.const_defined? @error_name
|
67
61
|
|
68
62
|
error_class = Object.const_get @error_name
|
@@ -1,14 +1,13 @@
|
|
1
1
|
require 'facets/string/interpolate'
|
2
2
|
require 'key_value_parser'
|
3
3
|
require 'shellwords'
|
4
|
-
require_relative 'jekyll_plugin_helper_class'
|
5
|
-
require_relative 'jekyll_plugin_helper_attribution'
|
6
4
|
|
7
5
|
module JekyllSupport
|
8
6
|
class JekyllPluginHelper
|
9
7
|
attr_accessor :liquid_context
|
10
|
-
attr_reader :
|
11
|
-
:
|
8
|
+
attr_reader :argument_string, :argv, :argv_original, :attribution, :excerpt_caller, :keys_values_original,
|
9
|
+
:keys_values, :jpsh_subclass_caller, :logger, :markup, :no_arg_parsing, :params, :params_original,
|
10
|
+
:tag_name
|
12
11
|
|
13
12
|
# See https://github.com/Shopify/liquid/wiki/Liquid-for-Programmers#create-your-own-tags
|
14
13
|
# @param tag_name [String] the name of the tag, which we already know.
|
@@ -22,8 +21,9 @@ module JekyllSupport
|
|
22
21
|
def initialize(tag_name, markup, logger, no_arg_parsing)
|
23
22
|
@tag_name = tag_name
|
24
23
|
@logger = logger
|
25
|
-
@
|
24
|
+
@markup = markup
|
26
25
|
@argument_string = markup
|
26
|
+
@no_arg_parsing = no_arg_parsing
|
27
27
|
rescue StandardError => e
|
28
28
|
e.shorten_backtrace
|
29
29
|
@logger.error { e.message }
|
@@ -66,8 +66,8 @@ module JekyllSupport
|
|
66
66
|
|
67
67
|
abort("Error: The #{tag_name} plugin is not an instance of JekyllSupport::JekyllBlock or JekyllSupport::JekyllTag") \
|
68
68
|
unless klass.instance_of?(Class) &&
|
69
|
-
(klass.ancestors.include?(JekyllSupport::JekyllBlock) ||
|
70
|
-
klass.ancestors.include?(JekyllSupport::JekyllTag))
|
69
|
+
(klass.ancestors.include?(::JekyllSupport::JekyllBlock) ||
|
70
|
+
klass.ancestors.include?(::JekyllSupport::JekyllTag))
|
71
71
|
|
72
72
|
Liquid::Template.register_tag(tag_name, klass)
|
73
73
|
return if quiet
|
data/lib/hooks/a_page.rb
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'jekyll_draft'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module JekyllSupport
|
6
|
+
# Contructor for testing and jekyll_outline
|
7
|
+
def self.apage_from( # rubocop:disable Metrics/ParameterLists
|
8
|
+
collection_name: nil,
|
9
|
+
date: nil,
|
10
|
+
description: nil,
|
11
|
+
draft: false,
|
12
|
+
last_modified: nil,
|
13
|
+
logger: nil,
|
14
|
+
order: nil,
|
15
|
+
title: nil,
|
16
|
+
url: nil
|
17
|
+
)
|
18
|
+
# Jekyll documents have inconsistent date and last_modified property types.
|
19
|
+
date = Time.parse(date) if date.instance_of?(String)
|
20
|
+
unless date.instance_of? Time
|
21
|
+
logger.error { "date is not an instance of Time, it is an instance of #{date.class}" }
|
22
|
+
exit 2
|
23
|
+
end
|
24
|
+
|
25
|
+
last_modified = Date.parse(last_modified) if last_modified.instance_of?(String)
|
26
|
+
last_modified = Date.parse(date.strftime('%Y-%m-%d')) if last_modified.nil?
|
27
|
+
unless last_modified.instance_of? Date
|
28
|
+
logger.error { "last_modified is not an instance of Date, it is an instance of #{last_modified.class}" }
|
29
|
+
exit 3
|
30
|
+
end
|
31
|
+
last_modified = Date.parse(date._to_s) if last_modified.nil?
|
32
|
+
data = {
|
33
|
+
collection: { label: collection_name },
|
34
|
+
draft: draft,
|
35
|
+
last_modified: last_modified,
|
36
|
+
order: order,
|
37
|
+
title: title,
|
38
|
+
}
|
39
|
+
obj = {}
|
40
|
+
JekyllSupport.new_attribute obj, :data, data
|
41
|
+
JekyllSupport.new_attribute obj, :date, date
|
42
|
+
JekyllSupport.new_attribute obj, :description, description
|
43
|
+
JekyllSupport.new_attribute obj, :draft, draft
|
44
|
+
JekyllSupport.new_attribute obj, :extname, '.html'
|
45
|
+
JekyllSupport.new_attribute obj, :last_modified, last_modified
|
46
|
+
JekyllSupport.new_attribute obj, :logger, PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config)
|
47
|
+
JekyllSupport.new_attribute obj, :title, title
|
48
|
+
JekyllSupport.new_attribute obj, :url, url
|
49
|
+
|
50
|
+
JekyllSupport::APage.new obj, nil
|
51
|
+
rescue StandardError => e
|
52
|
+
puts e.full_message
|
53
|
+
end
|
54
|
+
|
55
|
+
# Create Array of JekyllSupport::APage from objects
|
56
|
+
# @param objects [Array] An array of Jekyll::Document, Jekyll::Page or file names
|
57
|
+
# @param origin [String] Indicates type of objects being passed
|
58
|
+
def self.apages_from_objects(objects, origin)
|
59
|
+
pages = []
|
60
|
+
objects.each do |object|
|
61
|
+
unless object.respond_to?(:logger)
|
62
|
+
JekyllSupport.new_attribute(object,
|
63
|
+
:logger,
|
64
|
+
PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config))
|
65
|
+
end
|
66
|
+
page = APage.new(object, origin)
|
67
|
+
pages << page unless page.data['exclude_from_all'] || page.path == 'redirect.html'
|
68
|
+
end
|
69
|
+
pages
|
70
|
+
end
|
71
|
+
|
72
|
+
# Defines a new attribute called `prop_name` in object `obj` and sets it to `prop_value`
|
73
|
+
def self.new_attribute(obj, prop_name, prop_value)
|
74
|
+
obj.class.module_eval { attr_accessor prop_name }
|
75
|
+
obj.instance_variable_set :"@#{prop_name}", prop_value
|
76
|
+
end
|
77
|
+
|
78
|
+
FIXNUM_MAX = (2**((0.size * 8) - 2)) - 1 unless defined? FIXNUM_MAX
|
79
|
+
END_OF_DAYS = 1_000_000_000_000 unless defined? END_OF_DAYS # One trillion years in the future
|
80
|
+
# Time.new is -4712-01-01
|
81
|
+
|
82
|
+
class APage
|
83
|
+
attr_reader :categories, :collection_name, :content, :data, :date, :description, :destination, :draft,
|
84
|
+
:excerpt, :ext, :extname, :href, :label, :last_modified, :last_modified_field,
|
85
|
+
:layout, :logger, :name, :origin, :path, :relative_path, :tags, :title, :type, :url
|
86
|
+
|
87
|
+
# @param obj can be a `Jekyll::Document` or a Hash with properties
|
88
|
+
# @param origin values: 'collection', 'individual_page', and 'static_file'
|
89
|
+
# (See method JekyllSupport.apages_from_objects)
|
90
|
+
def initialize(obj, origin)
|
91
|
+
@logger = obj.logger
|
92
|
+
@origin = origin
|
93
|
+
build obj
|
94
|
+
rescue StandardError => e
|
95
|
+
::JekyllSupport.error_short_trace(@logger, e)
|
96
|
+
end
|
97
|
+
|
98
|
+
# @param name can be either a String or a Symbol
|
99
|
+
def field(name, use_default: true)
|
100
|
+
default_value = case name
|
101
|
+
when :date, :last_modified, :last_modified_at
|
102
|
+
END_OF_DAYS
|
103
|
+
else
|
104
|
+
''
|
105
|
+
end
|
106
|
+
|
107
|
+
result = data[name.to_sym] || data[name.to_s] if data.key?(name.to_sym) || data.key?(name.to_s)
|
108
|
+
return result if result
|
109
|
+
|
110
|
+
default_value if use_default
|
111
|
+
end
|
112
|
+
|
113
|
+
# Look within @data (if the property exists), then self for the given key as a symbol or a string
|
114
|
+
# @param key must be a symbol
|
115
|
+
# @return value of data[key] if key exists as a string or a symbol, else nil
|
116
|
+
def obj_field(obj, key)
|
117
|
+
if obj.respond_to? :data
|
118
|
+
return obj.data[key] if obj.data.key? key
|
119
|
+
|
120
|
+
return obj.data[key.to_s] if obj.data.key? key.to_s
|
121
|
+
end
|
122
|
+
return obj.send(key) if obj.respond_to?(key)
|
123
|
+
|
124
|
+
return unless obj.respond_to?(:key?)
|
125
|
+
return obj[key] if obj.key?(key)
|
126
|
+
|
127
|
+
obj[key.to_s] if obj.key?(key.to_s)
|
128
|
+
end
|
129
|
+
|
130
|
+
def order
|
131
|
+
if data.key?('order') || data.key?(:order)
|
132
|
+
data['order'] || data[:order]
|
133
|
+
else
|
134
|
+
FIXNUM_MAX
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def to_s
|
139
|
+
@label || @date.to_s
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
# Sets the following uninitialized instance attributes in APage from selected key/value pairs in `obj.data`:
|
145
|
+
# `categories`, `date`, `description`, `excerpt`, `ext`, `last_modified` or `last_modified_at`,
|
146
|
+
# `layout`, and `tags`.
|
147
|
+
# Sets the following instance attributes in APage from selected attributes in `obj` (when present):
|
148
|
+
# `content`, `destination`, `ext` and `extname`, `label` from `collection.label`,
|
149
|
+
# `path`, `relative_path`, `type`, and `url`.
|
150
|
+
def build(obj) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
151
|
+
@categories ||= obj_field(obj, :categories)
|
152
|
+
|
153
|
+
collection_value = obj_field(obj, :collection)
|
154
|
+
@collection_name = if collection_value
|
155
|
+
if collection_value.respond_to?(:label)
|
156
|
+
collection_value.label
|
157
|
+
elsif collection_value.key? :label
|
158
|
+
collection_value[:label]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
@content ||= obj.content if obj.respond_to? :content
|
163
|
+
@data ||= obj.respond_to?(:data) ? obj.data : {}
|
164
|
+
@date ||= obj_field(obj, :date) || Time.now # Jekyll doc.date property is a Time
|
165
|
+
@description ||= obj_field(obj, :description)
|
166
|
+
# TODO: What _config.yml setting should be passed to destination()?
|
167
|
+
@destination ||= obj.destination('') if obj.respond_to? :destination
|
168
|
+
@draft ||= Jekyll::Draft.draft? obj
|
169
|
+
@excerpt ||= obj_field(obj, :excerpt)
|
170
|
+
@ext ||= obj_field(obj, :ext) || obj_field(obj, :extname)
|
171
|
+
@extname ||= @ext # For compatibility with previous versions of all_collections
|
172
|
+
@label ||= obj.collection.label if obj.respond_to?(:collection) && obj.collection.respond_to?(:label)
|
173
|
+
|
174
|
+
@last_modified ||= obj_field(obj, :last_modified) ||
|
175
|
+
obj_field(obj, :last_modified_at) ||
|
176
|
+
Date.parse(@date.to_s) # Jekyll doc.last_modified property is a Date
|
177
|
+
|
178
|
+
@last_modified_field ||= if obj_field(obj, :last_modified)
|
179
|
+
:last_modified
|
180
|
+
elsif obj_field(obj, :last_modified_at)
|
181
|
+
:last_modified_at
|
182
|
+
end
|
183
|
+
|
184
|
+
@layout ||= obj_field(obj, :layout)
|
185
|
+
@path ||= obj_field(obj, :path)
|
186
|
+
@relative_path ||= obj_field(obj, :relative_path)
|
187
|
+
@tags ||= obj_field(obj, :tags)
|
188
|
+
@type ||= obj_field(obj, :type)
|
189
|
+
|
190
|
+
@url ||= obj.url
|
191
|
+
if @url
|
192
|
+
@url = "#{@url}index.html" if @url&.end_with? '/'
|
193
|
+
else
|
194
|
+
@url = '/index.html'
|
195
|
+
end
|
196
|
+
|
197
|
+
# @href = "/#{@href}" if @origin == 'individual_page'
|
198
|
+
@href ||= @url
|
199
|
+
@name ||= File.basename(@href)
|
200
|
+
@title ||= obj_field(obj, :title) || "<code>#{@href}</code>"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module JekyllAllCollections
|
2
|
+
module AllCollectionsHooks
|
3
|
+
class << self
|
4
|
+
attr_accessor :logger
|
5
|
+
end
|
6
|
+
@logger = PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config)
|
7
|
+
|
8
|
+
# No, all_collections is not defined for this hook
|
9
|
+
# Jekyll::Hooks.register(:site, :after_init, priority: :normal) do |site|
|
10
|
+
# defined_msg = ::AllCollectionsHooks.all_collections_defined?(site)
|
11
|
+
# @logger.debug { "Jekyll::Hooks.register(:site, :after_init: #{defined_msg}" }
|
12
|
+
# end
|
13
|
+
|
14
|
+
# Creates a `Array[APage]` property called site.all_collections if it does not already exist.
|
15
|
+
# The array is available from :site, :pre_render onwards
|
16
|
+
# Each `APage` entry is one document or page.
|
17
|
+
Jekyll::Hooks.register(:site, :post_read, priority: :normal) do |site|
|
18
|
+
@site = site
|
19
|
+
unless site.class.method_defined? :all_collections
|
20
|
+
defined_msg = ::AllCollectionsHooks.all_collections_defined? site
|
21
|
+
@logger.debug { "Jekyll::Hooks.register(:site, :post_read, :normal: #{defined_msg}" }
|
22
|
+
::AllCollectionsHooks.compute(site) if !@site.class.method_defined?(:all_documents) || @site.all_documents.nil?
|
23
|
+
end
|
24
|
+
rescue StandardError => e
|
25
|
+
::JekyllSupport.error_short_trace(@logger, e)
|
26
|
+
# ::JekyllSupport.warn_short_trace(@logger, e)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Yes, all_collections is defined for this hook
|
30
|
+
# Jekyll::Hooks.register(:site, :post_read, priority: :low) do |site|
|
31
|
+
# defined_msg = ::AllCollectionsHooks.all_collections_defined?(site)
|
32
|
+
# @logger.debug { "Jekyll::Hooks.register(:site, :post_read, :low: #{defined_msg}" }
|
33
|
+
# rescue StandardError => e
|
34
|
+
# ::JekyllSupport.error_short_trace(@logger, e)
|
35
|
+
# # ::JekyllSupport.warn_short_trace(@logger, e)
|
36
|
+
# end
|
37
|
+
|
38
|
+
# Yes, all_collections is defined for this hook
|
39
|
+
# Jekyll::Hooks.register(:site, :post_read, priority: :normal) do |site|
|
40
|
+
# defined_msg = ::AllCollectionsHooks.all_collections_defined?(site)
|
41
|
+
# @logger.debug { "Jekyll::Hooks.register(:site, :post_read, :normal: #{defined_msg}" }
|
42
|
+
# rescue StandardError => e
|
43
|
+
# ::JekyllSupport.error_short_trace(@logger, e)
|
44
|
+
# # ::JekyllSupport.warn_short_trace(@logger, e)
|
45
|
+
# end
|
46
|
+
|
47
|
+
# Yes, all_collections is defined for this hook
|
48
|
+
# Jekyll::Hooks.register(:site, :pre_render, priority: :normal) do |site, _payload|
|
49
|
+
# defined_msg = ::AllCollectionsHooks.all_collections_defined?(site)
|
50
|
+
# @logger.debug { "Jekyll::Hooks.register(:site, :pre_render: #{defined_msg}" }
|
51
|
+
# rescue StandardError => e
|
52
|
+
# ::JekyllSupport.error_short_trace(@logger, e)
|
53
|
+
# # ::JekyllSupport.warn_short_trace(@logger, e)
|
54
|
+
# end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
PluginMetaLogger.instance.logger.info do
|
59
|
+
"Loaded AllCollectionsHooks v#{JekyllPluginSupportVersion::VERSION} :site, :pre_render, :normal hook plugin."
|
60
|
+
end
|
61
|
+
Liquid::Template.register_filter(JekyllAllCollections::AllCollectionsHooks)
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Insert the url of a Jekyll::Page into each LruFile instance,
|
2
|
+
# along with the Page reference
|
3
|
+
# todo: replace references to url and :url with reverse_url and :reverse_url
|
4
|
+
LruFile = Struct.new(:url, :page) do
|
5
|
+
include SendChain
|
6
|
+
|
7
|
+
def <=>(other)
|
8
|
+
url <=> other.url
|
9
|
+
end
|
10
|
+
|
11
|
+
def href
|
12
|
+
url.reverse
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Matches suffixes of an array of urls
|
17
|
+
# Converts suffixes to prefixes
|
18
|
+
class SortedLruFiles
|
19
|
+
attr_reader :msbs
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@msbs = MSlinnBinarySearch.new %i[url start_with?]
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param apages [Array[APage]]
|
26
|
+
def add_pages(apages)
|
27
|
+
apages.each { |apage| insert apage.href, apage }
|
28
|
+
@msbs.enable_search
|
29
|
+
end
|
30
|
+
|
31
|
+
def enable_search
|
32
|
+
@msbs.enable_search
|
33
|
+
end
|
34
|
+
|
35
|
+
def find(suffix)
|
36
|
+
@msbs.find suffix
|
37
|
+
end
|
38
|
+
|
39
|
+
def insert(url, file)
|
40
|
+
lru_file = LruFile.new(url.reverse, file)
|
41
|
+
lru_file.new_chain [:url, %i[start_with? placeholder]]
|
42
|
+
@msbs.insert(lru_file)
|
43
|
+
end
|
44
|
+
|
45
|
+
def select(suffix)
|
46
|
+
@msbs.select_pages suffix
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,38 @@
|
|
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
|
+
# Called by early, high-priority hook.
|
14
|
+
# Computes site.all_collections, site.all_documents, site.everything, and site.sorted_lru_files
|
15
|
+
def self.compute(site)
|
16
|
+
site.class.module_eval { attr_accessor :all_collections, :all_documents, :everything, :sorted_lru_files }
|
17
|
+
|
18
|
+
documents = site.collections
|
19
|
+
.values
|
20
|
+
.map { |x| x.class.method_defined?(:docs) ? x.docs : x }
|
21
|
+
.flatten
|
22
|
+
.compact
|
23
|
+
@all_collections = JekyllSupport.apages_from_objects(documents, 'collection')
|
24
|
+
@all_documents = @all_collections +
|
25
|
+
JekyllSupport.apages_from_objects(site.pages, 'individual_page')
|
26
|
+
@everything = @all_documents +
|
27
|
+
JekyllSupport.apages_from_objects(site.static_files, 'static_file')
|
28
|
+
@sorted_lru_files = SortedLruFiles.new.add_pages @everything
|
29
|
+
|
30
|
+
site.all_collections = @all_collections
|
31
|
+
site.all_documents = @all_documents
|
32
|
+
site.everything = @everything
|
33
|
+
site.sorted_lru_files = @sorted_lru_files
|
34
|
+
rescue StandardError => e
|
35
|
+
::JekyllSupport.error_short_trace(::JekyllAllCollections::AllCollectionsHooks.logger, e)
|
36
|
+
# ::JekyllSupport.warn_short_trace(::JekyllAllCollections::AllCollectionsHooks.logger, e)
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
# @author Copyright 2020 Michael Slinn
|
4
|
+
# @license SPDX-License-Identifier: Apache-2.0
|
5
|
+
module JekyllAllCollections
|
6
|
+
PLUGIN_NAME = 'all_collections'.freeze unless defined?(PLUGIN_NAME)
|
7
|
+
CRITERIA = %w[date destination draft label last_modified last_modified_at path relative_path title type url].freeze unless defined?(CRITERIA)
|
8
|
+
DRAFT_HTML = '<i class="jekyll_draft">Draft</i>'.freeze unless defined?(DRAFT_HTML)
|
9
|
+
|
10
|
+
class AllCollectionsTag < ::JekyllSupport::JekyllTag
|
11
|
+
include ::JekyllPluginSupportVersion
|
12
|
+
|
13
|
+
# Method prescribed by JekyllTag.
|
14
|
+
# @return [String]
|
15
|
+
def render_impl
|
16
|
+
parse_arguments # Defines instance variables like @sort_by
|
17
|
+
sort_lambda = init_sort_by @sort_by
|
18
|
+
generate_output sort_lambda
|
19
|
+
rescue StandardError => e
|
20
|
+
::JekyllSupport.error_short_trace @logger, e
|
21
|
+
# ::JekyllSupport.warn_short_trace @logger, e
|
22
|
+
end
|
23
|
+
|
24
|
+
# Descending sort keys are preceded by a minus sign, and reverse the order of comparison
|
25
|
+
# @param criteria String Examples: 'date', '-date', 'last_modified', '-last_modified',
|
26
|
+
# ['date', 'last_modified], ['-date', '-last_modified'], ['date', '-last_modified']
|
27
|
+
# @return values:
|
28
|
+
# "->(a, b) { [a.last_modified] <=> [b.last_modified] }" (ascending)
|
29
|
+
# "->(a, b) { [b.last_modified] <=> [a.last_modified] }" (descending)
|
30
|
+
# "->(a, b) { [a.last_modified, a.date] <=> [b.last_modified, b.date] }" (descending last_modified, ascending date)
|
31
|
+
# "->(a, b) { [a.last_modified, b.date] <=> [b.last_modified, a.date] }" (ascending last_modified, descending date)
|
32
|
+
def self.create_lambda_string(criteria)
|
33
|
+
criteria_lhs_array = []
|
34
|
+
criteria_rhs_array = []
|
35
|
+
verify_sort_by_type(criteria).each do |c|
|
36
|
+
descending_sort = c.start_with? '-'
|
37
|
+
c.delete_prefix! '-'
|
38
|
+
abort("Error: '#{c}' is not a valid sort field. Valid field names are: #{CRITERIA.join ', '}") \
|
39
|
+
unless CRITERIA.include?(c)
|
40
|
+
criteria_lhs_array << (descending_sort ? "b.#{c}" : "a.#{c}")
|
41
|
+
criteria_rhs_array << (descending_sort ? "a.#{c}" : "b.#{c}")
|
42
|
+
end
|
43
|
+
"->(a, b) { [#{criteria_lhs_array.join(', ')}] <=> [#{criteria_rhs_array.join(', ')}] }"
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.verify_sort_by_type(sort_by)
|
47
|
+
case sort_by
|
48
|
+
when Array
|
49
|
+
sort_by
|
50
|
+
when Enumerable
|
51
|
+
sort_by.to_a
|
52
|
+
when Date
|
53
|
+
[sort_by.to_i]
|
54
|
+
when String
|
55
|
+
[sort_by]
|
56
|
+
else
|
57
|
+
abort "Error: @sort_by was specified as '#{sort_by}'; it must either be a string or an array of strings"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def default_head(sort_by)
|
64
|
+
criteria = (sort_by.map do |x|
|
65
|
+
reverse = x.start_with? '-'
|
66
|
+
criterion = x.delete_prefix('-').capitalize
|
67
|
+
criterion += reverse ? ' (Newest to Oldest)' : ' (Oldest to Newest)'
|
68
|
+
criterion
|
69
|
+
end).join(', ')
|
70
|
+
"All Posts in All Categories Sorted By #{criteria}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def evaluate(string)
|
74
|
+
self.eval string, binding, __FILE__, __LINE__
|
75
|
+
rescue StandardError => e
|
76
|
+
warn_short_trace e.red
|
77
|
+
end
|
78
|
+
|
79
|
+
def date_value(apage, field_name)
|
80
|
+
if %i[last_modified last_modified_at].include? field_name
|
81
|
+
apage.field(:last_modified_at, use_default: false) ||
|
82
|
+
apage.field(:last_modified, use_default: false) ||
|
83
|
+
Date.today
|
84
|
+
else
|
85
|
+
apage.date || Time.now
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def generate_output(sort_lambda)
|
90
|
+
id = @id.to_s.strip.empty? ? '' : " id=\"#{@id}\""
|
91
|
+
heading = @heading.strip.to_s.empty? ? '' : "<h2#{id}>#{@heading}</h2>"
|
92
|
+
apages = case @data_selector
|
93
|
+
when 'all_collections'
|
94
|
+
@site.all_collections
|
95
|
+
when 'all_documents'
|
96
|
+
@site.all_documents
|
97
|
+
when 'everything'
|
98
|
+
@site.everything
|
99
|
+
else
|
100
|
+
raise AllCollectionsError, "Invalid value for @data_selector (#{data_selector})"
|
101
|
+
end
|
102
|
+
filtered_apages = @collection_name.nil? ? apages : apages.select { |apage| apage.collection_name == @collection_name }
|
103
|
+
sorted_apages = filtered_apages.sort(&sort_lambda)
|
104
|
+
posts = sorted_apages.map do |apage|
|
105
|
+
date_column = @date_column.to_s == 'last_modified' ? :last_modified : :date
|
106
|
+
d = date_value(apage, date_column)
|
107
|
+
if d.respond_to?(:strftime)
|
108
|
+
date = d.strftime '%Y-%m-%d'
|
109
|
+
else
|
110
|
+
@logger.error do
|
111
|
+
"date_value returned a #{d.class} instead of a class with a strftime method like Date and Time; date_column=#{date_column}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
draft = apage.draft ? DRAFT_HTML : ''
|
115
|
+
title = apage.title || apage.href
|
116
|
+
href = "<a href='#{apage.href}'>#{title}</a>"
|
117
|
+
@logger.debug { " date='#{date}' #{title}\n" }
|
118
|
+
" <span>#{date}</span><span>#{href}#{draft}</span>"
|
119
|
+
end
|
120
|
+
<<~END_TEXT
|
121
|
+
#{heading}
|
122
|
+
<div class="posts">
|
123
|
+
#{posts.join "\n"}
|
124
|
+
</div>
|
125
|
+
END_TEXT
|
126
|
+
rescue NoMethodError || ArgumentError => e
|
127
|
+
::JekyllSupport.error_short_trace @logger, e
|
128
|
+
end
|
129
|
+
|
130
|
+
# See https://stackoverflow.com/a/75377832/553865
|
131
|
+
def init_sort_by(sort_by)
|
132
|
+
sort_lambda_string = AllCollectionsTag.create_lambda_string sort_by
|
133
|
+
|
134
|
+
@logger.debug do
|
135
|
+
"#{@page['path']} sort_lambda_string = #{sort_lambda_string}\n"
|
136
|
+
end
|
137
|
+
|
138
|
+
evaluate sort_lambda_string
|
139
|
+
end
|
140
|
+
|
141
|
+
# @return String defining the parsed sort_by expression
|
142
|
+
def parse_arguments
|
143
|
+
@collection_name = @helper.parameter_specified?('collection_name')
|
144
|
+
@data_selector = @helper.parameter_specified?('data_selector') || 'all_collections'
|
145
|
+
abort "Invalid data_selector #{@data_selector}" unless %w[all_collections all_documents everything].include? @data_selector
|
146
|
+
if (@data_selector != 'all_collections') && @collection_name
|
147
|
+
@logger.warn do
|
148
|
+
"collection_name was specified as '#{@collection_name}', but data_selector is #{@data_selector},
|
149
|
+
which is less effcient than specifying all_collections."
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
sort_by_param = @helper.parameter_specified? 'sort_by' # Might specify multiple sort fields
|
154
|
+
|
155
|
+
# Default to displaying last modified field unless a sort field is specified
|
156
|
+
@date_column = @helper.parameter_specified?('date_column') || 'last_modified'
|
157
|
+
unless %w[date last_modified].include?(@date_column)
|
158
|
+
raise AllCollectionsError "The date_column attribute must either have value 'date' or 'last_modified', " \
|
159
|
+
"but '#{@date_column}' was specified instead."
|
160
|
+
end
|
161
|
+
@date_column ||= (sort_by_param.include?('last_modified') ? 'last_modified' : 'date') # display the sort date by default
|
162
|
+
|
163
|
+
@id = @helper.parameter_specified?('id') || SecureRandom.hex(10)
|
164
|
+
|
165
|
+
@sort_by = (sort_by_param&.delete(' ')&.split(',') if sort_by_param != false) || ['-date']
|
166
|
+
|
167
|
+
@heading = @helper.parameter_specified?('heading') || default_head(@sort_by)
|
168
|
+
|
169
|
+
@sort_by
|
170
|
+
end
|
171
|
+
|
172
|
+
::JekyllSupport::JekyllPluginHelper.register(self, PLUGIN_NAME)
|
173
|
+
end
|
174
|
+
end
|