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.
- checksums.yaml +4 -4
- data/.rubocop.yml +17 -1
- data/CHANGELOG.md +33 -6
- data/README.md +787 -30
- data/jekyll_plugin_support.gemspec +13 -11
- data/lib/block/jekyll_plugin_support_block.rb +31 -15
- data/lib/block/jekyll_plugin_support_block_noarg.rb +0 -2
- data/lib/generator/jekyll_plugin_support_generator.rb +1 -7
- data/lib/helper/jekyll_plugin_helper.rb +6 -6
- data/lib/helper/jekyll_plugin_helper_class.rb +8 -3
- data/lib/hooks/a_page.rb +69 -0
- data/lib/hooks/all_collections_hooks.rb +61 -0
- data/lib/hooks/all_files.rb +48 -0
- data/lib/hooks/class_methods.rb +50 -0
- data/lib/jekyll_all_collections/all_collections_tag.rb +157 -0
- data/lib/jekyll_plugin_support/jekyll_plugin_support_class.rb +61 -28
- 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 +18 -13
- data/lib/tag/jekyll_plugin_support_tag.rb +26 -15
- 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 +112 -0
- data/spec/bsearch_spec.rb +50 -0
- data/spec/custom_error_spec.rb +9 -9
- data/spec/date_sort_spec.rb +84 -0
- data/spec/jekyll_plugin_helper_options_spec.rb +7 -3
- data/spec/liquid_variable_parsing_spec.rb +8 -8
- 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 +2 -0
- data/spec/status_persistence.txt +3 -9
- data/spec/testable_spec.rb +38 -0
- 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
|
-
#
|
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.
|
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
|
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
|
86
|
+
page = liquid_context.registers[:page]
|
91
87
|
envs = liquid_context.environments.first
|
92
88
|
layout = envs[:layout]
|
93
89
|
|
94
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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,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')]
|
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]
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
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
|
-
|
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.
|
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
|
-
|
59
|
-
@helper.reinitialize
|
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
|
@@ -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
|