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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +38 -3
  3. data/CHANGELOG.md +30 -6
  4. data/README.md +474 -4
  5. data/jekyll_plugin_support.gemspec +3 -1
  6. data/lib/block/jekyll_plugin_support_block.rb +5 -6
  7. data/lib/block/jekyll_plugin_support_block_noarg.rb +1 -3
  8. data/lib/error/jekyll_custom_error.rb +6 -5
  9. data/lib/generator/jekyll_plugin_support_generator.rb +1 -7
  10. data/lib/helper/jekyll_plugin_helper.rb +5 -5
  11. data/lib/helper/jekyll_plugin_helper_class.rb +2 -2
  12. data/lib/hooks/a_page.rb +203 -0
  13. data/lib/hooks/all_collections_hooks.rb +61 -0
  14. data/lib/hooks/all_files.rb +48 -0
  15. data/lib/hooks/class_methods.rb +38 -0
  16. data/lib/jekyll_all_collections/all_collections_tag.rb +174 -0
  17. data/lib/jekyll_plugin_support/jekyll_plugin_support_class.rb +6 -7
  18. data/lib/jekyll_plugin_support/jekyll_plugin_support_spec_support.rb +1 -3
  19. data/lib/jekyll_plugin_support/version.rb +1 -1
  20. data/lib/jekyll_plugin_support.rb +27 -12
  21. data/lib/tag/jekyll_plugin_support_tag.rb +1 -4
  22. data/lib/tag/jekyll_plugin_support_tag_noarg.rb +0 -2
  23. data/lib/util/mslinn_binary_search.rb +152 -0
  24. data/lib/util/send_chain.rb +56 -0
  25. data/spec/all_collections_tag/all_collections_tag_sort_spec.rb +184 -0
  26. data/spec/bsearch_spec.rb +50 -0
  27. data/spec/custom_error_spec.rb +12 -10
  28. data/spec/date_sort_spec.rb +84 -0
  29. data/spec/jekyll_plugin_helper_options_spec.rb +9 -3
  30. data/spec/liquid_variable_parsing_spec.rb +7 -6
  31. data/spec/mslinn_binary_search_spec.rb +47 -0
  32. data/spec/send_chain_spec.rb +72 -0
  33. data/spec/send_spec.rb +28 -0
  34. data/spec/sorted_lru_files_spec.rb +82 -0
  35. data/spec/spec_helper.rb +15 -3
  36. data/spec/status_persistence.txt +4 -7
  37. data/spec/testable_spec.rb +38 -0
  38. 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 { "#{e.class} on line #{@line_number} #{of_page} while processing #{tag_name} #{in_file_name}- #{e.message}" }
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 = 3)
46
- b = backtrace[0..backtrace_element_count - 1].map do |x|
47
- x.gsub(Dir.pwd + '/', './')
48
- end
49
- set_backtrace b
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 :argv, :attribution, :keys_values, :logger, :markup, :no_arg_parsing, :params, :tag_name,
11
- :argv_original, :excerpt_caller, :keys_values_original, :params_original, :jpsh_subclass_caller
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
- @no_arg_parsing = no_arg_parsing
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
@@ -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