jekyll_plugin_support 1.0.3 → 3.0.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +17 -1
  3. data/CHANGELOG.md +33 -6
  4. data/README.md +787 -30
  5. data/jekyll_plugin_support.gemspec +13 -11
  6. data/lib/block/jekyll_plugin_support_block.rb +31 -15
  7. data/lib/block/jekyll_plugin_support_block_noarg.rb +0 -2
  8. data/lib/generator/jekyll_plugin_support_generator.rb +1 -7
  9. data/lib/helper/jekyll_plugin_helper.rb +6 -6
  10. data/lib/helper/jekyll_plugin_helper_class.rb +8 -3
  11. data/lib/hooks/a_page.rb +69 -0
  12. data/lib/hooks/all_collections_hooks.rb +61 -0
  13. data/lib/hooks/all_files.rb +48 -0
  14. data/lib/hooks/class_methods.rb +50 -0
  15. data/lib/jekyll_all_collections/all_collections_tag.rb +157 -0
  16. data/lib/jekyll_plugin_support/jekyll_plugin_support_class.rb +61 -28
  17. data/lib/jekyll_plugin_support/jekyll_plugin_support_spec_support.rb +1 -3
  18. data/lib/jekyll_plugin_support/version.rb +1 -1
  19. data/lib/jekyll_plugin_support.rb +18 -13
  20. data/lib/tag/jekyll_plugin_support_tag.rb +26 -15
  21. data/lib/tag/jekyll_plugin_support_tag_noarg.rb +0 -2
  22. data/lib/util/mslinn_binary_search.rb +152 -0
  23. data/lib/util/send_chain.rb +56 -0
  24. data/spec/all_collections_tag/all_collections_tag_sort_spec.rb +112 -0
  25. data/spec/bsearch_spec.rb +50 -0
  26. data/spec/custom_error_spec.rb +9 -9
  27. data/spec/date_sort_spec.rb +84 -0
  28. data/spec/jekyll_plugin_helper_options_spec.rb +7 -3
  29. data/spec/liquid_variable_parsing_spec.rb +8 -8
  30. data/spec/mslinn_binary_search_spec.rb +47 -0
  31. data/spec/send_chain_spec.rb +72 -0
  32. data/spec/send_spec.rb +28 -0
  33. data/spec/sorted_lru_files_spec.rb +82 -0
  34. data/spec/spec_helper.rb +2 -0
  35. data/spec/status_persistence.txt +3 -9
  36. data/spec/testable_spec.rb +38 -0
  37. metadata +42 -5
@@ -3,12 +3,12 @@ require_relative 'lib/jekyll_plugin_support/version'
3
3
  Gem::Specification.new do |spec|
4
4
  github = 'https://github.com/mslinn/jekyll_plugin_support'
5
5
 
6
- spec.bindir = 'exe'
7
- spec.authors = ['Mike Slinn']
8
- spec.email = ['mslinn@mslinn.com']
9
- spec.files = Dir['.rubocop.yml', 'LICENSE.*', 'Rakefile', '{lib,spec}/**/*', '*.gemspec', '*.md']
6
+ spec.bindir = 'exe'
7
+ spec.authors = ['Mike Slinn']
8
+ spec.email = ['mslinn@mslinn.com']
9
+ spec.files = Dir['.rubocop.yml', 'LICENSE.*', 'Rakefile', '{lib,spec}/**/*', '*.gemspec', '*.md']
10
10
  spec.homepage = 'https://www.mslinn.com/jekyll_plugins/jekyll_plugin_support.html'
11
- spec.license = 'MIT'
11
+ spec.license = 'MIT'
12
12
  spec.metadata = {
13
13
  'allowed_push_host' => 'https://rubygems.org',
14
14
  'bug_tracker_uri' => "#{github}/issues",
@@ -16,21 +16,23 @@ Gem::Specification.new do |spec|
16
16
  'homepage_uri' => spec.homepage,
17
17
  'source_code_uri' => github,
18
18
  }
19
- spec.name = 'jekyll_plugin_support'
19
+ spec.name = 'jekyll_plugin_support'
20
+ spec.platform = Gem::Platform::RUBY
20
21
  spec.post_install_message = <<~END_MESSAGE
21
22
 
22
23
  Thanks for installing #{spec.name}!
23
24
 
24
25
  END_MESSAGE
25
- spec.require_paths = ['lib']
26
+ spec.require_paths = ['lib']
26
27
  spec.required_ruby_version = '>= 2.6.0'
27
- spec.summary = 'Provides a framework for writing and testing Jekyll plugins'
28
- spec.test_files = spec.files.grep %r{^(test|spec|features)/}
29
- spec.version = JekyllPluginSupportVersion::VERSION
28
+ spec.summary = 'Provides a framework for writing and testing Jekyll plugins'
29
+ spec.test_files = spec.files.grep %r{^(test|spec|features)/}
30
+ spec.version = JekyllPluginSupportVersion::VERSION
30
31
 
31
32
  spec.add_dependency 'facets'
32
- spec.add_dependency 'jekyll', '>= 3.5.0'
33
+ spec.add_dependency 'jekyll', '>= 4.4.1'
33
34
  spec.add_dependency 'jekyll_plugin_logger'
34
35
  spec.add_dependency 'key-value-parser'
35
36
  spec.add_dependency 'pry'
37
+ spec.add_dependency 'sorted_set'
36
38
  end
@@ -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
@@ -8,22 +6,34 @@ module JekyllSupport
8
6
  # See https://github.com/Shopify/liquid/wiki/Liquid-for-Programmers#create-your-own-tags
9
7
  # @param tag_name [String] the name of the tag, which we usually know.
10
8
  # @param argument_string [String] the arguments passed to the tag, as a single string.
11
- # @param parse_context [Liquid::ParseContext] hash that stores Liquid options.
12
- # By default it has two keys: :locale and :line_numbers, the first is a Liquid::I18n object, and the second,
13
- # a boolean parameter that determines if error messages should display the line number the error occurred.
14
- # This argument is used mostly to display localized error messages on Liquid built-in Tags and Filters.
15
- # See https://github.com/Shopify/liquid/wiki/Liquid-for-Programmers#create-your-own-tags
9
+ # @param parse_context [Liquid::ParseContext] contains the following attributes:
10
+ # @depth might have the value 0
11
+ # @error_mode might have the value `:strict`
12
+ # @line_number duplicates @ptions[:line_number]
13
+ # @locale duplicates @ptions[:locale]
14
+ # @options is a hash with the following two keys that holds Liquid options:
15
+ # :locale is a Liquid::I18n object, used to display localized error messages on Liquid built-in tags and filters.
16
+ # :line_number is the line number containing the plugin invocation.
17
+ # See https://github.com/Shopify/liquid/wiki/Liquid-for-Programmers#create-your-own-tags
18
+ # @partial Boolean, unclear what this indicates
19
+ # @template_options Replicates @options
20
+ # @trim_whitespace might have the value `false`
21
+ # @warnings array
16
22
  # @return [void]
17
23
  def initialize(tag_name, markup, parse_context)
18
24
  super
19
25
  @tag_name = tag_name
20
- @argument_string = markup.to_s # Vars in plugin parameters cannot be replaced yet
26
+ raise JekyllPluginSupportError, "markup is a #{markup.class} with value '#{markup}'." unless markup.instance_of? String
27
+
28
+ # Lookup variable names with values in markup in render because site and config are not available here
29
+ @argument_string = markup # Replace variable names with values in markup in render because site and config are not available here
30
+
21
31
  @logger = PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config)
22
32
  @logger.debug { "#{self.class}: respond_to?(:no_arg_parsing) #{respond_to?(:no_arg_parsing) ? 'yes' : 'no'}." }
23
33
  @helper = JekyllPluginHelper.new tag_name, markup, @logger, respond_to?(:no_arg_parsing)
24
34
 
25
35
  @error_name = "#{tag_name.camelcase(:upper)}Error"
26
- JekyllSupport::CustomError.factory @error_name
36
+ ::JekyllSupport::CustomError.factory @error_name
27
37
  end
28
38
 
29
39
  # Liquid::Block subclasses do not render if there is no content within the tag
@@ -36,11 +46,11 @@ module JekyllSupport
36
46
  # Defines @config, @envs, @mode, @page and @site
37
47
  # @return [String]
38
48
  def render(liquid_context)
39
- @helper.liquid_context = JekyllSupport.inject_vars @logger, liquid_context
49
+ @helper.liquid_context = ::JekyllSupport.inject_config_vars liquid_context # modifies liquid_context
40
50
  text = super # Liquid variable values in content are looked up and substituted
41
51
 
42
52
  @envs = liquid_context.environments.first
43
- @page = liquid_context.registers[:page] # hash
53
+ @page = liquid_context.registers[:page]
44
54
  @scopes = liquid_context.scopes
45
55
  @site = liquid_context.registers[:site]
46
56
 
@@ -51,6 +61,7 @@ module JekyllSupport
51
61
 
52
62
  set_error_context
53
63
 
64
+ # @envs.keys are :content, :highlighter_prefix, :highlighter_suffix, :jekyll, :layout, :page, :paginator, :site, :theme
54
65
  @layout = @envs[:layout]
55
66
  @paginator = @envs[:paginator]
56
67
  @theme = @envs[:theme]
@@ -58,18 +69,23 @@ module JekyllSupport
58
69
  env = @config['env']
59
70
  @mode = env&.key?('JEKYLL_ENV') ? env['JEKYLL_ENV'] : 'development'
60
71
 
61
- @helper.reinitialize @markup.strip
72
+ @argument_string = JekyllSupport.lookup_liquid_variables @logger, @helper.liquid_context, @argument_string.to_s.strip
73
+ @helper.reinitialize @argument_string.to_s.strip
62
74
 
63
75
  @attribution = @helper.parameter_specified?('attribution') || false unless @no_arg_parsing
64
76
  @logger.debug { "@keys_values='#{@keys_values}'" }
65
77
 
66
- markup = JekyllSupport.lookup_liquid_variables liquid_context, @argument_string
67
- @helper.reinitialize markup
78
+ # @argument_string = JekyllSupport.lookup_liquid_variables @logger, liquid_context, @argument_string # Is this redundant?
79
+ # @argument_string.strip! # Is this redundant?
80
+ # @helper.reinitialize @argument_string # Is this redundant?
68
81
 
69
82
  render_impl(text)
70
83
  rescue StandardError => e
71
84
  e.shorten_backtrace
72
- @logger.error { "#{e.class} on line #{@line_number} of #{e.backtrace[0].split(':').first} by #{@tag_name} - #{e.message}" }
85
+ file_name = e.backtrace[0]&.split(':')&.first
86
+ in_file_name = "in '#{file_name}' " if file_name
87
+ of_page = "of '#{@page['path']}'" if @page
88
+ @logger.error { "#{e.class} on line #{@line_number} #{of_page} while processing #{tag_name} #{in_file_name}- #{e.message}" }
73
89
  binding.pry if @pry_on_standard_error # rubocop:disable Lint/Debugger
74
90
  raise e if @die_on_standard_error
75
91
 
@@ -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
@@ -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
26
24
  @markup = markup
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 }
@@ -42,7 +42,7 @@ module JekyllSupport
42
42
 
43
43
  def reinitialize(markup)
44
44
  # @keys_values was a Hash[Symbol, String|Boolean] but now it is Hash[String, String|Boolean]
45
- @markup = markup
45
+ @argument_string = markup
46
46
  if @no_arg_parsing
47
47
  define_singleton_method(:argv) { warn_fetch :argv }
48
48
  define_singleton_method(:keys_values) { warn_fetch :keys_values }
@@ -55,7 +55,10 @@ module JekyllSupport
55
55
  END_MSG
56
56
  end
57
57
 
58
- def self.register(klass, tag_name)
58
+ # @param klass [Class] class instance to register
59
+ # @param tag_name [String] name of plugin defined by klass to register as
60
+ # @param quiet [Boolean] suppress registration message if truthy
61
+ def self.register(klass, tag_name, quiet: false)
59
62
  abort("Error: The #{tag_name} plugin does not define VERSION") \
60
63
  unless klass.const_defined?(:VERSION)
61
64
 
@@ -63,10 +66,12 @@ module JekyllSupport
63
66
 
64
67
  abort("Error: The #{tag_name} plugin is not an instance of JekyllSupport::JekyllBlock or JekyllSupport::JekyllTag") \
65
68
  unless klass.instance_of?(Class) &&
66
- (klass.ancestors.include?(JekyllSupport::JekyllBlock) ||
67
- klass.ancestors.include?(JekyllSupport::JekyllTag))
69
+ (klass.ancestors.include?(::JekyllSupport::JekyllBlock) ||
70
+ klass.ancestors.include?(::JekyllSupport::JekyllTag))
68
71
 
69
72
  Liquid::Template.register_tag(tag_name, klass)
73
+ return if quiet
74
+
70
75
  msg = generate_message(klass, tag_name, version)
71
76
  PluginMetaLogger.instance.info { msg }
72
77
  end
@@ -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,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,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(::JekyllAllCollections::AllCollectionsHooks.logger, e)
48
+ # JekyllSupport.warn_short_trace(::JekyllAllCollections::AllCollectionsHooks.logger, e)
49
+ end
50
+ end
@@ -0,0 +1,157 @@
1
+ # require 'jekyll_draft'
2
+ require 'securerandom'
3
+
4
+ # @author Copyright 2020 Michael Slinn
5
+ # @license SPDX-License-Identifier: Apache-2.0
6
+ module JekyllAllCollections
7
+ PLUGIN_NAME = 'all_collections'.freeze unless defined?(PLUGIN_NAME)
8
+ CRITERIA = %w[date destination draft label last_modified last_modified_at path relative_path title type url].freeze unless defined?(CRITERIA)
9
+ DRAFT_HTML = '<i class="jekyll_draft">Draft</i>'.freeze unless defined?(DRAFT_HTML)
10
+
11
+ class AllCollectionsTag < ::JekyllSupport::JekyllTag
12
+ include ::JekyllPluginSupportVersion
13
+
14
+ # Method prescribed by JekyllTag.
15
+ # @return [String]
16
+ def render_impl
17
+ parse_arguments # Defines instance variables like @sort_by
18
+ sort_lambda = init_sort_by @sort_by, @sort_by_param
19
+ @heading = @helper.parameter_specified?('heading') || default_head(@sort_by)
20
+ generate_output sort_lambda
21
+ rescue StandardError => e
22
+ JekyllSupport.error_short_trace @logger, e
23
+ # JekyllSupport.warn_short_trace @logger, e
24
+ end
25
+
26
+ # Descending sort keys reverse the order of comparison
27
+ # Example return values:
28
+ # "->(a, b) { [a.last_modified] <=> [b.last_modified] }"
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 last_modified_value(apage)
80
+ @logger.debug do
81
+ " apage.last_modified='#{apage.last_modified}'; " \
82
+ "apage.last_modified_at='#{apage.last_modified_at}'; " \
83
+ "@date_column='#{@date_column}'"
84
+ end
85
+ last_modified = if @date_column == 'last_modified' && apage.respond_to?(:last_modified)
86
+ apage.last_modified
87
+ elsif apage.respond_to? :last_modified_at
88
+ apage.last_modified_at
89
+ else
90
+ apage.date
91
+ end
92
+ last_modified ||= apage.date || Date.today
93
+ last_modified
94
+ end
95
+
96
+ def generate_output(sort_lambda)
97
+ id = @id.to_s.strip.empty? ? '' : " id='#{@id}'"
98
+ heading = @heading.strip.to_s.empty? ? '' : "<h2#{id}>#{@heading}</h2>"
99
+ data = case @data_selector
100
+ when 'all_collections'
101
+ @site.all_collections
102
+ when 'all_documents'
103
+ @site.all_documents
104
+ when 'everything'
105
+ @site.everything
106
+ else
107
+ raise AllCollectionsError, "Invalid value for @data_selector (#{data_selector})"
108
+ end
109
+ collection = data.sort(&sort_lambda)
110
+ posts = collection.map do |x|
111
+ last_modified = last_modified_value x
112
+ date = last_modified.strftime '%Y-%m-%d'
113
+ draft = x.draft ? DRAFT_HTML : ''
114
+ href = "<a href='#{x.href}'>#{x.title}</a>"
115
+ @logger.debug { " date='#{date}' #{x.title}\n" }
116
+ " <span>#{date}</span><span>#{href}#{draft}</span>"
117
+ end
118
+ <<~END_TEXT
119
+ #{heading}
120
+ <div class="posts">
121
+ #{posts.join "\n"}
122
+ </div>
123
+ END_TEXT
124
+ rescue ArgumentError => e
125
+ warn_short_trace e
126
+ end
127
+
128
+ # See https://stackoverflow.com/a/75377832/553865
129
+ def init_sort_by(sort_by, sort_by_param)
130
+ sort_lambda_string = AllCollectionsTag.create_lambda_string sort_by
131
+
132
+ @logger.debug do
133
+ "#{@page['path']} sort_by_param=#{sort_by_param} " \
134
+ "sort_lambda_string = #{sort_lambda_string}\n"
135
+ end
136
+
137
+ evaluate sort_lambda_string
138
+ end
139
+
140
+ def parse_arguments
141
+ @data_selector = @helper.parameter_specified?('data_selector') || 'all_collections'
142
+ abort "Invalid data_selector #{@data_selector}" unless %w[all_collections all_documents everything].include? @data_selector
143
+
144
+ @date_column = @helper.parameter_specified?('date_column') || 'date'
145
+ unless %w[date last_modified].include?(@date_column)
146
+ raise AllCollectionsError "The date_column attribute must either have value 'date' or 'last_modified', " \
147
+ "but '#{@date_column}' was specified"
148
+ end
149
+
150
+ @id = @helper.parameter_specified?('id') || SecureRandom.hex(10)
151
+ @sort_by_param = @helper.parameter_specified? 'sort_by'
152
+ @sort_by = (@sort_by_param&.delete(' ')&.split(',') if @sort_by_param != false) || ['-date']
153
+ end
154
+
155
+ ::JekyllSupport::JekyllPluginHelper.register(self, PLUGIN_NAME)
156
+ end
157
+ end