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
@@ -1,5 +1,3 @@
1
- require_relative '../error/jekyll_custom_error'
2
-
3
1
  # Monkey patch StandardError so a new method called shorten_backtrace is added.
4
2
  class StandardError
5
3
  def shorten_backtrace(backtrace_element_count = 3)
@@ -12,7 +10,7 @@ class StandardError
12
10
  end
13
11
 
14
12
  module JekyllSupport
15
- DISPLAYED_CALLS = 8
13
+ DISPLAYED_CALLS = 8 unless defined?(DISPLAYED_CALLS)
16
14
 
17
15
  def self.error_short_trace(logger, error)
18
16
  error.set_backtrace error.backtrace[0..DISPLAYED_CALLS]
@@ -22,11 +20,11 @@ module JekyllSupport
22
20
 
23
21
  # @return a new StandardError subclass containing the shorten_backtrace method
24
22
  def define_error
25
- Class.new JekyllSupport::CustomError
23
+ Class.new ::JekyllSupport::CustomError
26
24
  end
27
25
  module_function :define_error
28
26
 
29
- JekyllPluginSupportError = define_error
27
+ JekyllPluginSupportError = define_error unless defined?(JekyllPluginSupportError)
30
28
 
31
29
  def self.dump_vars(_logger, liquid_context)
32
30
  page = liquid_context.registers[:page]
@@ -39,23 +37,21 @@ module JekyllSupport
39
37
  END_MSG
40
38
  end
41
39
 
42
- # Add variable definitions from _config.yml to liquid_context
43
- # Modifies liquid_context in the caller
40
+ # Inject variable definitions from _config.yml into liquid_context
41
+ # Modifies liquid_context.scopes in the caller
44
42
  # (call by object reference, see https://stackoverflow.com/a/1872159/553865)
45
43
  # @return modified liquid_context
46
44
  # See README.md#configuration-variable-definitions
47
45
  # See demo/variables.html
48
- def self.inject_vars(_logger, liquid_context)
49
- # TODO: Modify a deep clone? Do I dare?
46
+ def self.inject_config_vars(liquid_context)
50
47
  site = liquid_context.registers[:site]
51
48
 
52
49
  plugin_variables = site.config['liquid_vars']
53
- return liquid_context unless plugin_variables
54
50
 
55
51
  scope = liquid_context.scopes.last
56
52
 
57
53
  env = site.config['env']
58
- mode = env&.key?('JEKYLL_ENV') ? env['JEKYLL_ENV'] : 'development'
54
+ @mode = env&.key?('JEKYLL_ENV') ? env['JEKYLL_ENV'] : 'development'
59
55
 
60
56
  # Set default values
61
57
  plugin_variables&.each do |name, value|
@@ -63,7 +59,7 @@ module JekyllSupport
63
59
  end
64
60
 
65
61
  # Override with environment-specific values
66
- plugin_variables[mode]&.each do |name, value|
62
+ plugin_variables&.[](@mode)&.each do |name, value|
67
63
  scope[name] = value if value.instance_of? String
68
64
  end
69
65
 
@@ -85,34 +81,71 @@ module JekyllSupport
85
81
  # Modifies a clone of markup_original so variable references are replaced by their values
86
82
  # @param markup_original to be cloned
87
83
  # @return modified markup_original
88
- def self.lookup_liquid_variables(liquid_context, markup_original)
84
+ def self.lookup_liquid_variables(logger, liquid_context, markup_original)
89
85
  markup = markup_original.clone
90
- page = liquid_context.registers[:page]
86
+ page = liquid_context.registers[:page]
91
87
  envs = liquid_context.environments.first
92
88
  layout = envs[:layout]
93
89
 
94
- # process layout variables
90
+ markup = process_layout_variables logger, layout, markup
91
+ markup = process_page_variables logger, page, markup
92
+ liquid_context.scopes&.each do |scope|
93
+ markup = process_included_variables logger, scope, markup
94
+ markup = process_liquid_variables logger, scope, markup
95
+ end
96
+ markup
97
+ rescue StandardError => e
98
+ logger.error { e.full_message }
99
+ end
100
+
101
+ def self.process_included_variables(logger, scope, markup)
102
+ scope['include']&.each do |name, value|
103
+ if value.nil?
104
+ value = ''
105
+ logger.warn { "include.#{name} is undefined." }
106
+ end
107
+ markup.gsub!("{{include.#{name}}}", value)
108
+ end
109
+ markup
110
+ rescue StandardError => e
111
+ logger.error { e.full_message }
112
+ end
113
+
114
+ def self.process_layout_variables(logger, layout, markup)
95
115
  layout&.each do |name, value|
116
+ if value.nil?
117
+ value = ''
118
+ logger.warn { "layout.#{value} is undefined." }
119
+ end
96
120
  markup.gsub!("{{layout.#{name}}}", value.to_s)
97
121
  end
122
+ markup
123
+ rescue StandardError => e
124
+ logger.error { e.full_message }
125
+ end
98
126
 
99
- # process page variables
100
- # puts "\nStarting page variable processing of #{page['path']}; stack has #{caller.length} elements".green
101
- keys = page.keys
102
- %w[excerpt output].each { |key| keys.delete key }
103
- # puts " Filtered keys: #{keys.join ' '}"
104
- # keys.each { |key| puts " #{key}: #{page[key]}" }
105
- keys&.each do |key|
106
- markup.gsub!("{{page.#{key}}}", page[key].to_s)
127
+ # Process assigned, captured and injected variables
128
+ def self.process_liquid_variables(logger, scope, markup)
129
+ scope&.each do |name, value|
130
+ next if name.nil?
131
+
132
+ value = '' if value.nil?
133
+ markup.gsub!("{{#{name}}}", value&.to_s)
107
134
  end
135
+ markup
136
+ rescue StandardError => e
137
+ logger.error { e.full_message }
138
+ end
108
139
 
109
- # Process assigned, captured and injected variables
110
- liquid_context.scopes.each do |scope|
111
- scope&.each do |name, value|
112
- markup.gsub!("{{#{name}}}", value.to_s)
113
- end
140
+ def self.process_page_variables(logger, page, markup)
141
+ page&.each_key do |key|
142
+ next if %w[content excerpt next previous output].include? key # Skip problem attributes
143
+
144
+ markup.gsub!("{{page.#{key}}}", page[key].to_s)
114
145
  end
115
146
  markup
147
+ rescue StandardError => e
148
+ logger.error { e.full_message }
116
149
  end
117
150
 
118
151
  def self.warn_short_trace(logger, error)
@@ -1,6 +1,4 @@
1
- require 'jekyll'
2
-
3
- Registers = Struct.new(:page, :site)
1
+ Registers = Struct.new(:page, :site) unless defined?(Registers)
4
2
 
5
3
  # Mock for Collections
6
4
  class Collections
@@ -1,3 +1,3 @@
1
1
  module JekyllPluginSupportVersion
2
- VERSION = '1.0.3'.freeze
2
+ VERSION = '3.0.0'.freeze unless defined?(VERSION)
3
3
  end
@@ -1,13 +1,26 @@
1
- require 'colorator'
2
- require 'jekyll'
3
- require 'jekyll_plugin_logger'
4
-
5
1
  def require_directory(dir)
6
- Dir[File.join(dir, '*.rb')].sort.each do |file|
2
+ Dir[File.join(dir, '*.rb')]&.sort&.each do |file|
7
3
  require file unless file == __FILE__
8
4
  end
9
5
  end
10
6
 
7
+ require 'colorator'
8
+ require 'jekyll'
9
+ require 'jekyll_plugin_logger'
10
+ require 'pry'
11
+ require 'sorted_set'
12
+
13
+ # require_directory __dir__
14
+ require_directory "#{__dir__}/util"
15
+ require_directory "#{__dir__}/error"
16
+ require_directory "#{__dir__}/block"
17
+ require_directory "#{__dir__}/generator"
18
+ require_directory "#{__dir__}/helper"
19
+ require_directory "#{__dir__}/jekyll_plugin_support"
20
+ require_directory "#{__dir__}/tag"
21
+ require_directory "#{__dir__}/jekyll_all_collections"
22
+ require_directory "#{__dir__}/hooks"
23
+
11
24
  module JekyllSupport
12
25
  def self.redef_without_warning(const, value)
13
26
  send(:remove_const, const) if const_defined?(const)
@@ -21,14 +34,6 @@ module NoArgParsing
21
34
  @no_arg_parsing = true
22
35
  end
23
36
 
24
- require_directory __dir__
25
- require_directory "#{__dir__}/block"
26
- require_directory "#{__dir__}/error"
27
- require_directory "#{__dir__}/generator"
28
- require_directory "#{__dir__}/helper"
29
- require_directory "#{__dir__}/jekyll_plugin_support"
30
- require_directory "#{__dir__}/tag"
31
-
32
37
  module JekyllSupport
33
38
  class JekyllTag
34
39
  include JekyllSupportError
@@ -1,6 +1,3 @@
1
- require 'pry'
2
- require_relative '../error/jekyll_plugin_error_handling'
3
-
4
1
  module JekyllSupport
5
2
  # Base class for Jekyll tags
6
3
  class JekyllTag < Liquid::Tag
@@ -9,31 +6,41 @@ module JekyllSupport
9
6
  # See https://github.com/Shopify/liquid/wiki/Liquid-for-Programmers#create-your-own-tags
10
7
  # @param tag_name [String] the name of the tag, which we usually know.
11
8
  # @param argument_string [String] the arguments passed to the tag, as a single string.
12
- # @param parse_context [Liquid::ParseContext] hash that stores Liquid options.
13
- # By default it has two keys: :locale and :line_numbers, the first is a Liquid::I18n object, and the second,
14
- # a boolean parameter that determines if error messages should display the line number the error occurred.
15
- # This argument is used mostly to display localized error messages on Liquid built-in Tags and Filters.
16
- # 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
17
22
  # @return [void]
18
23
  def initialize(tag_name, markup, parse_context)
19
24
  super
20
25
  @tag_name = tag_name
21
26
  raise JekyllPluginSupportError, "markup is a #{markup.class} with value '#{markup}'." unless markup.instance_of? String
22
27
 
23
- @argument_string = markup
28
+ # Vars in plugin parameters cannot be replaced yet
29
+ @argument_string = markup.to_s # Lookup variable names with values in markup in render because site and config are not available here
30
+
24
31
  @logger = PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config)
25
32
  @logger.debug { "#{self.class}: respond_to?(:no_arg_parsing) #{respond_to?(:no_arg_parsing) ? 'yes' : 'no'}." }
26
33
  @helper = JekyllPluginHelper.new(tag_name, @argument_string, @logger, respond_to?(:no_arg_parsing))
27
34
 
28
35
  @error_name = "#{tag_name.camelcase(:upper)}Error"
29
- JekyllSupport::CustomError.factory @error_name
36
+ ::JekyllSupport::CustomError.factory @error_name
30
37
  end
31
38
 
32
39
  # Method prescribed by the Jekyll plugin lifecycle.
33
40
  def render(liquid_context)
34
41
  return if @helper.excerpt_caller
35
42
 
36
- @helper.liquid_context = JekyllSupport.inject_vars @logger, liquid_context
43
+ @helper.liquid_context = JekyllSupport.inject_config_vars liquid_context # modifies liquid_context
37
44
 
38
45
  @envs = liquid_context.environments.first
39
46
  @page = liquid_context.registers[:page]
@@ -55,8 +62,12 @@ module JekyllSupport
55
62
  env = @config['env']
56
63
  @mode = env&.key?('JEKYLL_ENV') ? env['JEKYLL_ENV'] : 'development'
57
64
 
58
- markup = JekyllSupport.lookup_liquid_variables liquid_context, @argument_string
59
- @helper.reinitialize markup.strip
65
+ @argument_string = JekyllSupport.lookup_liquid_variables @logger, @helper.liquid_context, @argument_string.to_s.strip
66
+ @helper.reinitialize @argument_string.to_s.strip
67
+
68
+ # @argument_string = JekyllSupport.lookup_liquid_variables @logger, liquid_context, @argument_string # Is this redundant?
69
+ # @argument_string.strip! # Is this redundant?
70
+ # @helper.reinitialize @argument_string # Is this redundant?
60
71
 
61
72
  render_impl
62
73
  rescue StandardError => e
@@ -64,13 +75,13 @@ module JekyllSupport
64
75
  file_name = e.backtrace[0]&.split(':')&.first
65
76
  in_file_name = "in '#{file_name}' " if file_name
66
77
  of_page = "of '#{@page['path']}'" if @page
67
- @logger.error { "#{e.class} on line #{@line_number} #{of_page}while processing #{tag_name} #{in_file_name}- #{e.message}" }
78
+ @logger.error { "#{e.class} on line #{@line_number} #{of_page} while processing #{tag_name} #{in_file_name}- #{e.message}" }
68
79
  binding.pry if @pry_on_standard_error # rubocop:disable Lint/Debugger
69
80
  raise e if @die_on_standard_error
70
81
 
71
82
  <<~END_MSG
72
83
  <div class='standard_error'>
73
- #{e.class} on line #{@line_number} #{of_page}while processing #{tag_name} #{in_file_name} - #{e.message}
84
+ #{e.class} on line #{@line_number} #{of_page} while processing #{tag_name} #{in_file_name} - #{JekyllPluginHelper.remove_html_tags e.message}
74
85
  </div>
75
86
  END_MSG
76
87
  end
@@ -1,5 +1,3 @@
1
- require_relative '../error/jekyll_plugin_error_handling'
2
-
3
1
  module JekyllSupport
4
2
  class JekyllTagNoArgParsing < JekyllTag
5
3
  attr_reader :argument_string, :helper, :line_number, :logger, :page, :site
@@ -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,56 @@
1
+ # Supports one chain at a time
2
+ module SendChain
3
+ # See https://stackoverflow.com/a/79333706/553865
4
+ # This method can be called directly if no methods in the chain require arguments
5
+ # Does not use any external state
6
+ def send_chain(chain)
7
+ Array(chain).inject(self) { |o, a| o.send(*a) }
8
+ end
9
+
10
+ # Saves @chain structure containing :placeholders for arguments to be supplied later
11
+ # Call when a different chain with :placeholders is desired
12
+ def new_chain(chain)
13
+ abort "new_chain error: chain must be an array ('#{chain}' was an #{chain.class.name})" \
14
+ unless chain.instance_of?(Array)
15
+ @chain = chain
16
+ end
17
+
18
+ # Call after new_chain, to evaluate @chain with values
19
+ def substitute_and_send_chain_with(values)
20
+ send_chain substitute_chain_with values
21
+ end
22
+
23
+ alias evaluate_with substitute_and_send_chain_with
24
+
25
+ # Call this method after calling new_chain to perform error checking and replace :placeholders with values.
26
+ # @chain is not modified.
27
+ # @return [Array] Modified chain
28
+ def substitute_chain_with(values)
29
+ values = [values] unless values.instance_of?(Array)
30
+
31
+ placeholder_count = @chain.flatten.count { |x| x == :placeholder }
32
+ if values.length != placeholder_count
33
+ abort "with_values error: number of values (#{values.length}) does not match the number of placeholders (#{placeholder_count})"
34
+ end
35
+
36
+ eval_chain @chain, values
37
+ end
38
+
39
+ private
40
+
41
+ # Replaces :placeholders with values
42
+ # Does not use any external state
43
+ # @return modified chain
44
+ def eval_chain(chain, values)
45
+ chain.map do |c|
46
+ case c
47
+ when :placeholder
48
+ values.shift
49
+ when Array
50
+ eval_chain c, values
51
+ else
52
+ c
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,112 @@
1
+ require 'spec_helper'
2
+ require_relative '../../lib/jekyll_all_collections'
3
+
4
+ class APageStub
5
+ attr_reader :date, :last_modified, :label
6
+
7
+ def initialize(date, last_modified, label = '')
8
+ @date = Date.parse date
9
+ @last_modified = Date.parse last_modified
10
+ @label = label
11
+ end
12
+
13
+ def to_s
14
+ @label
15
+ end
16
+ end
17
+
18
+ def show(lambda_string, result, expected)
19
+ p "For lambda_string: #{lambda_string}"
20
+ p " result: #{result.map(&:label).join(', ')} <==> expected: #{expected.map(&:label).join(', ')}"
21
+ end
22
+
23
+ # See https://stackoverflow.com/a/75388137/553865
24
+ RSpec.describe(AllCollectionsTag::AllCollectionsTag) do
25
+ let(:o1) { APageStub.new('2020-01-01', '2020-01-01', 'a_A') }
26
+ let(:o2) { APageStub.new('2021-01-01', '2020-01-01', 'b_A') }
27
+ let(:o3) { APageStub.new('2021-01-01', '2023-01-01', 'b_B') }
28
+ let(:o4) { APageStub.new('2022-01-01', '2023-01-01', 'c_B') }
29
+ let(:objs) { [o1, o2, o3, o4] }
30
+
31
+ it 'defines sort_by lambda with last_modified' do
32
+ sort_lambda = ->(a, b) { [a.last_modified] <=> [b.last_modified] }
33
+ result = objs.sort(&sort_lambda)
34
+ expect(result).to eq([o1, o2, o3, o4])
35
+ end
36
+
37
+ it 'makes sort_by lambdas from stringified date' do
38
+ sort_lambda = eval '->(a, b) { a.last_modified <=> b.last_modified }',
39
+ NullBinding.new.min_binding, __FILE__, __LINE__ - 1
40
+ result = objs.sort(&sort_lambda)
41
+ expect(result).to eq([o1, o2, o3, o4])
42
+ end
43
+
44
+ it 'makes sort_by lambdas from stringified array of last_modified' do
45
+ sort_lambda = eval '->(a, b) { [a.last_modified] <=> [b.last_modified] }',
46
+ NullBinding.new.min_binding, __FILE__, __LINE__ - 1
47
+ result = objs.sort(&sort_lambda)
48
+ expect(result).to eq([o1, o2, o3, o4])
49
+ end
50
+
51
+ it 'makes sort_by lambdas with descending keys from stringified array of last_modified' do
52
+ sort_lambda = eval '->(a, b) { [b.last_modified] <=> [a.last_modified] }',
53
+ NullBinding.new.min_binding, __FILE__, __LINE__ - 1
54
+ result = objs.sort(&sort_lambda)
55
+ expected = [o3, o4, o1, o2]
56
+ expect(result).to eq(expected)
57
+ end
58
+
59
+ it 'create_lambda with 1 date key, descending' do
60
+ lambda_string = described_class.create_lambda_string('-last_modified')
61
+ sort_lambda = described_class.evaluate(lambda_string)
62
+ result = objs.sort(&sort_lambda)
63
+ expected = [o3, o4, o1, o2]
64
+ # show(lambda_string, result, expected)
65
+ expect(result).to eq(expected)
66
+ end
67
+
68
+ it 'create_lambda with 1 date key, ascending' do
69
+ lambda_string = described_class.create_lambda_string('date')
70
+ sort_lambda = described_class.evaluate(lambda_string)
71
+ result = objs.sort(&sort_lambda)
72
+ expected = [o1, o2, o3, o4]
73
+ # show(lambda_string, result, expected)
74
+ expect(result).to eq(expected)
75
+ end
76
+
77
+ it 'create_lambda with 2 date keys, both ascending' do
78
+ lambda_string = described_class.create_lambda_string(%w[date last_modified])
79
+ sort_lambda = described_class.evaluate(lambda_string)
80
+ result = objs.sort(&sort_lambda)
81
+ expected = [o1, o2, o3, o4]
82
+ # show(lambda_string, result, expected)
83
+ expect(result).to eq(expected)
84
+ end
85
+
86
+ it 'create_lambda with 2 date keys, both descending' do
87
+ lambda_string = described_class.create_lambda_string(['-date', '-last_modified'])
88
+ sort_lambda = described_class.evaluate(lambda_string)
89
+ result = objs.sort(&sort_lambda)
90
+ expected = [o4, o3, o2, o1]
91
+ # show(lambda_string, result, expected)
92
+ expect(result).to eq(expected)
93
+ end
94
+
95
+ it 'create_lambda with 2 date keys, first descending and second ascending' do
96
+ lambda_string = described_class.create_lambda_string(['-date', 'last_modified'])
97
+ sort_lambda = described_class.evaluate(lambda_string)
98
+ result = objs.sort(&sort_lambda)
99
+ expected = [o4, o2, o3, o1]
100
+ # show(lambda_string, result, expected)
101
+ expect(result).to eq(expected)
102
+ end
103
+
104
+ it 'create_lambda with 2 date keys, first ascending and second descending' do
105
+ lambda_string = described_class.create_lambda_string(['date', '-last_modified'])
106
+ sort_lambda = described_class.evaluate(lambda_string)
107
+ result = objs.sort(&sort_lambda)
108
+ expected = [o1, o3, o2, o4]
109
+ # show(lambda_string, result, expected)
110
+ expect(result).to eq(expected)
111
+ end
112
+ end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ # Ruby's binary search is unsuitable because ordering requirements are not stable.
4
+ # the value to be searched for changes the required ordering
5
+
6
+ RSpec.describe(Array) do
7
+ before { skip('Never gonna give you up/Never gonna let you down') }
8
+
9
+ sorted_ints = [0, 4, 7, 10, 12]
10
+ sorted_strings = %w[aaa aab aac bbb bbc bbd ccc ccd cce].sort.reverse
11
+
12
+ it 'returns index of first int match' do
13
+ actual = sorted_ints.bsearch_index { |x| x >= 4 }
14
+ expect(actual).to eq(1)
15
+
16
+ actual = sorted_ints.bsearch_index { |x| x >= 6 }
17
+ expect(actual).to eq(2)
18
+
19
+ actual = sorted_ints.bsearch_index { |x| x >= -1 }
20
+ expect(actual).to eq(0)
21
+
22
+ actual = sorted_ints.bsearch_index { |x| x >= 100 }
23
+ expect(actual).to be_nil
24
+ end
25
+
26
+ # See https://stackoverflow.com/q/79333097/553865
27
+ it 'returns index of first string match' do
28
+ puts(sorted_strings.map { |x| x.start_with? 'a' })
29
+ index = sorted_strings.bsearch_index { |x| x.start_with? 'a' }
30
+ expect(sorted_strings[index]).to eq('aac')
31
+
32
+ index = sorted_strings.bsearch_index { |x| x.start_with? 'aa' }
33
+ expect(sorted_strings[index]).to eq('aac')
34
+
35
+ index = sorted_strings.bsearch_index { |x| x.start_with? 'aaa' }
36
+ expect(sorted_strings[index]).to eq('aaa')
37
+
38
+ index = sorted_strings.bsearch_index { |x| x.start_with? 'b' }
39
+ expect(sorted_strings[index]).to eq('bbd')
40
+
41
+ index = sorted_strings.bsearch_index { |x| x.start_with? 'bb' }
42
+ expect(sorted_strings[index]).to eq('bbd')
43
+
44
+ index = sorted_strings.bsearch_index { |x| x.start_with? 'bbc' }
45
+ expect(sorted_strings[index]).to eq('bbc')
46
+
47
+ index = sorted_strings.bsearch_index { |x| x.start_with? 'cc' }
48
+ expect(sorted_strings[index]).to eq('cce')
49
+ end
50
+ end