jekyll 4.0.0.pre.beta1 → 4.2.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +204 -18
  3. data/README.markdown +2 -6
  4. data/lib/blank_template/_layouts/default.html +1 -1
  5. data/lib/jekyll.rb +6 -17
  6. data/lib/jekyll/cleaner.rb +3 -3
  7. data/lib/jekyll/collection.rb +2 -2
  8. data/lib/jekyll/command.rb +4 -2
  9. data/lib/jekyll/commands/doctor.rb +19 -15
  10. data/lib/jekyll/commands/new.rb +4 -4
  11. data/lib/jekyll/commands/new_theme.rb +0 -2
  12. data/lib/jekyll/commands/serve.rb +12 -1
  13. data/lib/jekyll/configuration.rb +18 -19
  14. data/lib/jekyll/converters/identity.rb +2 -2
  15. data/lib/jekyll/converters/markdown/kramdown_parser.rb +70 -1
  16. data/lib/jekyll/convertible.rb +30 -23
  17. data/lib/jekyll/document.rb +41 -19
  18. data/lib/jekyll/drops/collection_drop.rb +3 -3
  19. data/lib/jekyll/drops/document_drop.rb +4 -3
  20. data/lib/jekyll/drops/drop.rb +98 -20
  21. data/lib/jekyll/drops/site_drop.rb +3 -3
  22. data/lib/jekyll/drops/static_file_drop.rb +4 -4
  23. data/lib/jekyll/drops/url_drop.rb +11 -3
  24. data/lib/jekyll/entry_filter.rb +18 -7
  25. data/lib/jekyll/excerpt.rb +1 -1
  26. data/lib/jekyll/filters.rb +112 -28
  27. data/lib/jekyll/filters/url_filters.rb +45 -15
  28. data/lib/jekyll/frontmatter_defaults.rb +14 -19
  29. data/lib/jekyll/hooks.rb +22 -21
  30. data/lib/jekyll/inclusion.rb +32 -0
  31. data/lib/jekyll/layout.rb +5 -0
  32. data/lib/jekyll/liquid_renderer.rb +18 -15
  33. data/lib/jekyll/liquid_renderer/file.rb +10 -0
  34. data/lib/jekyll/liquid_renderer/table.rb +1 -64
  35. data/lib/jekyll/page.rb +42 -11
  36. data/lib/jekyll/page_excerpt.rb +25 -0
  37. data/lib/jekyll/path_manager.rb +53 -10
  38. data/lib/jekyll/profiler.rb +58 -0
  39. data/lib/jekyll/reader.rb +11 -6
  40. data/lib/jekyll/readers/collection_reader.rb +1 -0
  41. data/lib/jekyll/readers/data_reader.rb +4 -0
  42. data/lib/jekyll/readers/layout_reader.rb +1 -0
  43. data/lib/jekyll/readers/page_reader.rb +1 -0
  44. data/lib/jekyll/readers/post_reader.rb +2 -1
  45. data/lib/jekyll/readers/static_file_reader.rb +1 -0
  46. data/lib/jekyll/readers/theme_assets_reader.rb +1 -0
  47. data/lib/jekyll/related_posts.rb +1 -1
  48. data/lib/jekyll/renderer.rb +15 -17
  49. data/lib/jekyll/site.rb +34 -10
  50. data/lib/jekyll/static_file.rb +17 -12
  51. data/lib/jekyll/tags/include.rb +82 -33
  52. data/lib/jekyll/tags/link.rb +2 -1
  53. data/lib/jekyll/tags/post_url.rb +3 -4
  54. data/lib/jekyll/theme.rb +6 -8
  55. data/lib/jekyll/url.rb +8 -5
  56. data/lib/jekyll/utils.rb +5 -5
  57. data/lib/jekyll/utils/platforms.rb +34 -49
  58. data/lib/jekyll/utils/win_tz.rb +1 -1
  59. data/lib/jekyll/version.rb +1 -1
  60. data/lib/theme_template/theme.gemspec.erb +1 -4
  61. metadata +34 -39
@@ -116,7 +116,7 @@ module Jekyll
116
116
  #
117
117
  # Returns the output extension
118
118
  def output_ext
119
- @output_ext ||= Jekyll::Renderer.new(site, self).output_ext
119
+ renderer.output_ext
120
120
  end
121
121
 
122
122
  # The base filename of the document, without the file extname.
@@ -133,6 +133,10 @@ module Jekyll
133
133
  @basename ||= File.basename(path)
134
134
  end
135
135
 
136
+ def renderer
137
+ @renderer ||= Jekyll::Renderer.new(site, self)
138
+ end
139
+
136
140
  # Produces a "cleaned" relative path.
137
141
  # The "cleaned" relative path is the relative path without the extname
138
142
  # and with the collection's directory removed as well.
@@ -253,14 +257,16 @@ module Jekyll
253
257
  #
254
258
  # Returns the full path to the output file of this document.
255
259
  def destination(base_directory)
256
- dest = site.in_dest_dir(base_directory)
257
- path = site.in_dest_dir(dest, URL.unescape_path(url))
258
- if url.end_with? "/"
259
- path = File.join(path, "index.html")
260
- else
261
- path << output_ext unless path.end_with? output_ext
260
+ @destination ||= {}
261
+ @destination[base_directory] ||= begin
262
+ path = site.in_dest_dir(base_directory, URL.unescape_path(url))
263
+ if url.end_with? "/"
264
+ path = File.join(path, "index.html")
265
+ else
266
+ path << output_ext unless path.end_with? output_ext
267
+ end
268
+ path
262
269
  end
263
- path
264
270
  end
265
271
 
266
272
  # Write the generated Document file to the destination directory.
@@ -298,7 +304,7 @@ module Jekyll
298
304
  else
299
305
  begin
300
306
  merge_defaults
301
- read_content(opts)
307
+ read_content(**opts)
302
308
  read_post_data
303
309
  rescue StandardError => e
304
310
  handle_read_error(e)
@@ -347,9 +353,14 @@ module Jekyll
347
353
  # True if the document has a collection and if that collection's #write?
348
354
  # method returns true, and if the site's Publisher will publish the document.
349
355
  # False otherwise.
356
+ #
357
+ # rubocop:disable Naming/MemoizedInstanceVariableName
350
358
  def write?
351
- collection&.write? && site.publisher.publish?(self)
359
+ return @write_p if defined?(@write_p)
360
+
361
+ @write_p = collection&.write? && site.publisher.publish?(self)
352
362
  end
363
+ # rubocop:enable Naming/MemoizedInstanceVariableName
353
364
 
354
365
  # The Document excerpt_separator, from the YAML Front-Matter or site
355
366
  # default excerpt_separator value
@@ -414,9 +425,13 @@ module Jekyll
414
425
  #
415
426
  # Returns nothing.
416
427
  def categories_from_path(special_dir)
417
- superdirs = relative_path.sub(Document.superdirs_regex(special_dir), "")
418
- superdirs = superdirs.split(File::SEPARATOR)
419
- superdirs.reject! { |c| c.empty? || c == special_dir || c == basename }
428
+ if relative_path.start_with?(special_dir)
429
+ superdirs = []
430
+ else
431
+ superdirs = relative_path.sub(Document.superdirs_regex(special_dir), "")
432
+ superdirs = superdirs.split(File::SEPARATOR)
433
+ superdirs.reject! { |c| c.empty? || c == special_dir || c == basename }
434
+ end
420
435
 
421
436
  merge_data!({ "categories" => superdirs }, :source => "file path")
422
437
  end
@@ -429,14 +444,14 @@ module Jekyll
429
444
  categories.flatten!
430
445
  categories.uniq!
431
446
 
432
- merge_data!("categories" => categories)
447
+ merge_data!({ "categories" => categories })
433
448
  end
434
449
 
435
450
  def populate_tags
436
451
  tags = Utils.pluralized_array_from_hash(data, "tag", "tags")
437
452
  tags.flatten!
438
453
 
439
- merge_data!("tags" => tags)
454
+ merge_data!({ "tags" => tags })
440
455
  end
441
456
 
442
457
  private
@@ -444,7 +459,10 @@ module Jekyll
444
459
  def merge_categories!(other)
445
460
  if other.key?("categories") && !other["categories"].nil?
446
461
  other["categories"] = other["categories"].split if other["categories"].is_a?(String)
447
- other["categories"] = (data["categories"] || []) | other["categories"]
462
+
463
+ if data["categories"].is_a?(Array)
464
+ other["categories"] = data["categories"] | other["categories"]
465
+ end
448
466
  end
449
467
  end
450
468
 
@@ -462,10 +480,10 @@ module Jekyll
462
480
  merge_data!(defaults, :source => "front matter defaults") unless defaults.empty?
463
481
  end
464
482
 
465
- def read_content(opts)
466
- self.content = File.read(path, Utils.merged_file_read_opts(site, opts))
483
+ def read_content(**opts)
484
+ self.content = File.read(path, **Utils.merged_file_read_opts(site, opts))
467
485
  if content =~ YAML_FRONT_MATTER_REGEXP
468
- self.content = $POSTMATCH
486
+ self.content = Regexp.last_match.post_match
469
487
  data_file = SafeYAML.load(Regexp.last_match(1))
470
488
  merge_data!(data_file, :source => "YAML front matter") if data_file
471
489
  end
@@ -497,6 +515,10 @@ module Jekyll
497
515
  elsif relative_path =~ DATELESS_FILENAME_MATCHER
498
516
  slug, ext = Regexp.last_match.captures
499
517
  end
518
+ # `slug` will be nil for documents without an extension since the regex patterns
519
+ # above tests for an extension as well.
520
+ # In such cases, assign `basename_without_ext` as the slug.
521
+ slug ||= basename_without_ext
500
522
 
501
523
  # slugs shouldn't end with a period
502
524
  # `String#gsub!` removes all trailing periods (in comparison to `String#chomp!`)
@@ -7,10 +7,10 @@ module Jekyll
7
7
 
8
8
  mutable false
9
9
 
10
- def_delegator :@obj, :write?, :output
11
- def_delegators :@obj, :label, :docs, :files, :directory, :relative_directory
10
+ delegate_method_as :write?, :output
11
+ delegate_methods :label, :docs, :files, :directory, :relative_directory
12
12
 
13
- private def_delegator :@obj, :metadata, :fallback_data
13
+ private delegate_method_as :metadata, :fallback_data
14
14
 
15
15
  def to_s
16
16
  docs.to_s
@@ -11,10 +11,11 @@ module Jekyll
11
11
 
12
12
  mutable false
13
13
 
14
- def_delegator :@obj, :relative_path, :path
15
- def_delegators :@obj, :id, :output, :content, :to_s, :relative_path, :url, :date
14
+ delegate_method_as :relative_path, :path
15
+ private delegate_method_as :data, :fallback_data
16
16
 
17
- private def_delegator :@obj, :data, :fallback_data
17
+ delegate_methods :id, :output, :content, :to_s, :relative_path, :url, :date
18
+ data_delegators "title", "categories", "tags"
18
19
 
19
20
  def collection
20
21
  @obj.collection.label
@@ -6,20 +6,101 @@ module Jekyll
6
6
  include Enumerable
7
7
 
8
8
  NON_CONTENT_METHODS = [:fallback_data, :collapse_document].freeze
9
+ NON_CONTENT_METHOD_NAMES = NON_CONTENT_METHODS.map(&:to_s).freeze
10
+ private_constant :NON_CONTENT_METHOD_NAMES
9
11
 
10
- # Get or set whether the drop class is mutable.
11
- # Mutability determines whether or not pre-defined fields may be
12
- # overwritten.
13
- #
14
- # is_mutable - Boolean set mutability of the class (default: nil)
15
- #
16
- # Returns the mutability of the class
17
- def self.mutable(is_mutable = nil)
18
- @is_mutable = is_mutable || false
19
- end
12
+ # A private stash to avoid repeatedly generating the setter method name string for
13
+ # a call to `Drops::Drop#[]=`.
14
+ # The keys of the stash below have a very high probability of being called upon during
15
+ # the course of various `Jekyll::Renderer#run` calls.
16
+ SETTER_KEYS_STASH = {
17
+ "content" => "content=",
18
+ "layout" => "layout=",
19
+ "page" => "page=",
20
+ "paginator" => "paginator=",
21
+ "highlighter_prefix" => "highlighter_prefix=",
22
+ "highlighter_suffix" => "highlighter_suffix=",
23
+ }.freeze
24
+ private_constant :SETTER_KEYS_STASH
25
+
26
+ class << self
27
+ # Get or set whether the drop class is mutable.
28
+ # Mutability determines whether or not pre-defined fields may be
29
+ # overwritten.
30
+ #
31
+ # is_mutable - Boolean set mutability of the class (default: nil)
32
+ #
33
+ # Returns the mutability of the class
34
+ def mutable(is_mutable = nil)
35
+ @is_mutable = is_mutable || false
36
+ end
37
+
38
+ def mutable?
39
+ @is_mutable
40
+ end
41
+
42
+ # public delegation helper methods that calls onto Drop's instance
43
+ # variable `@obj`.
44
+
45
+ # Generate private Drop instance_methods for each symbol in the given list.
46
+ #
47
+ # Returns nothing.
48
+ def private_delegate_methods(*symbols)
49
+ symbols.each { |symbol| private delegate_method(symbol) }
50
+ nil
51
+ end
52
+
53
+ # Generate public Drop instance_methods for each symbol in the given list.
54
+ #
55
+ # Returns nothing.
56
+ def delegate_methods(*symbols)
57
+ symbols.each { |symbol| delegate_method(symbol) }
58
+ nil
59
+ end
20
60
 
21
- def self.mutable?
22
- @is_mutable
61
+ # Generate public Drop instance_method for given symbol that calls `@obj.<sym>`.
62
+ #
63
+ # Returns delegated method symbol.
64
+ def delegate_method(symbol)
65
+ define_method(symbol) { @obj.send(symbol) }
66
+ end
67
+
68
+ # Generate public Drop instance_method named `delegate` that calls `@obj.<original>`.
69
+ #
70
+ # Returns delegated method symbol.
71
+ def delegate_method_as(original, delegate)
72
+ define_method(delegate) { @obj.send(original) }
73
+ end
74
+
75
+ # Generate public Drop instance_methods for each string entry in the given list.
76
+ # The generated method(s) access(es) `@obj`'s data hash.
77
+ #
78
+ # Returns nothing.
79
+ def data_delegators(*strings)
80
+ strings.each do |key|
81
+ data_delegator(key) if key.is_a?(String)
82
+ end
83
+ nil
84
+ end
85
+
86
+ # Generate public Drop instance_methods for given string `key`.
87
+ # The generated method access(es) `@obj`'s data hash.
88
+ #
89
+ # Returns method symbol.
90
+ def data_delegator(key)
91
+ define_method(key.to_sym) { @obj.data[key] }
92
+ end
93
+
94
+ # Array of stringified instance methods that do not end with the assignment operator.
95
+ #
96
+ # (<klass>.instance_methods always generates a new Array object so it can be mutated)
97
+ #
98
+ # Returns array of strings.
99
+ def getter_method_names
100
+ @getter_method_names ||= instance_methods.map!(&:to_s).tap do |list|
101
+ list.reject! { |item| item.end_with?("=") }
102
+ end
103
+ end
23
104
  end
24
105
 
25
106
  # Create a new Drop
@@ -65,7 +146,7 @@ module Jekyll
65
146
  # and the key matches a method in which case it raises a
66
147
  # DropMutationException.
67
148
  def []=(key, val)
68
- setter = "#{key}="
149
+ setter = SETTER_KEYS_STASH[key] || "#{key}="
69
150
  if respond_to?(setter)
70
151
  public_send(setter, val)
71
152
  elsif respond_to?(key.to_s)
@@ -84,13 +165,10 @@ module Jekyll
84
165
  #
85
166
  # Returns an Array of strings which represent method-specific keys.
86
167
  def content_methods
87
- @content_methods ||= (
88
- self.class.instance_methods \
89
- - Jekyll::Drops::Drop.instance_methods \
90
- - NON_CONTENT_METHODS
91
- ).map(&:to_s).reject do |method|
92
- method.end_with?("=")
93
- end
168
+ @content_methods ||= \
169
+ self.class.getter_method_names \
170
+ - Jekyll::Drops::Drop.getter_method_names \
171
+ - NON_CONTENT_METHOD_NAMES
94
172
  end
95
173
 
96
174
  # Check if key exists in Drop
@@ -7,10 +7,10 @@ module Jekyll
7
7
 
8
8
  mutable false
9
9
 
10
- def_delegator :@obj, :site_data, :data
11
- def_delegators :@obj, :time, :pages, :static_files, :tags, :categories
10
+ delegate_method_as :site_data, :data
11
+ delegate_methods :time, :pages, :static_files, :tags, :categories
12
12
 
13
- private def_delegator :@obj, :config, :fallback_data
13
+ private delegate_method_as :config, :fallback_data
14
14
 
15
15
  def [](key)
16
16
  if key != "posts" && @obj.collections.key?(key)
@@ -4,11 +4,11 @@ module Jekyll
4
4
  module Drops
5
5
  class StaticFileDrop < Drop
6
6
  extend Forwardable
7
- def_delegators :@obj, :name, :extname, :modified_time, :basename
8
- def_delegator :@obj, :relative_path, :path
9
- def_delegator :@obj, :type, :collection
7
+ delegate_methods :name, :extname, :modified_time, :basename
8
+ delegate_method_as :relative_path, :path
9
+ delegate_method_as :type, :collection
10
10
 
11
- private def_delegator :@obj, :data, :fallback_data
11
+ private delegate_method_as :data, :fallback_data
12
12
  end
13
13
  end
14
14
  end
@@ -7,8 +7,8 @@ module Jekyll
7
7
 
8
8
  mutable false
9
9
 
10
- def_delegator :@obj, :cleaned_relative_path, :path
11
- def_delegator :@obj, :output_ext, :output_ext
10
+ delegate_method :output_ext
11
+ delegate_method_as :cleaned_relative_path, :path
12
12
 
13
13
  def collection
14
14
  @obj.collection.label
@@ -35,6 +35,14 @@ module Jekyll
35
35
  category_set.to_a.join("/")
36
36
  end
37
37
 
38
+ # Similar to output from #categories, but each category will be downcased and
39
+ # all non-alphanumeric characters of the category replaced with a hyphen.
40
+ def slugified_categories
41
+ Array(@obj.data["categories"]).each_with_object(Set.new) do |category, set|
42
+ set << Utils.slugify(category.to_s)
43
+ end.to_a.join("/")
44
+ end
45
+
38
46
  # CCYY
39
47
  def year
40
48
  @obj.date.strftime("%Y")
@@ -125,7 +133,7 @@ module Jekyll
125
133
  private
126
134
 
127
135
  def fallback_data
128
- {}
136
+ @fallback_data ||= {}
129
137
  end
130
138
  end
131
139
  end
@@ -3,6 +3,7 @@
3
3
  module Jekyll
4
4
  class EntryFilter
5
5
  attr_reader :site
6
+
6
7
  SPECIAL_LEADING_CHAR_REGEX = %r!\A#{Regexp.union([".", "_", "#", "~"])}!o.freeze
7
8
 
8
9
  def initialize(site, base_directory = nil)
@@ -32,13 +33,21 @@ module Jekyll
32
33
  # Reject this entry if it is just a "dot" representation.
33
34
  # e.g.: '.', '..', '_movies/.', 'music/..', etc
34
35
  next true if e.end_with?(".")
35
- # Reject this entry if it is a symlink.
36
+
37
+ # Check if the current entry is explicitly included and cache the result
38
+ included = included?(e)
39
+
40
+ # Reject current entry if it is excluded but not explicitly included as well.
41
+ next true if excluded?(e) && !included
42
+
43
+ # Reject current entry if it is a symlink.
36
44
  next true if symlink?(e)
37
- # Do not reject this entry if it is included.
38
- next false if included?(e)
39
45
 
40
- # Reject this entry if it is special, a backup file, or excluded.
41
- special?(e) || backup?(e) || excluded?(e)
46
+ # Do not reject current entry if it is explicitly included.
47
+ next false if included
48
+
49
+ # Reject current entry if it is special or a backup file.
50
+ special?(e) || backup?(e)
42
51
  end
43
52
  end
44
53
 
@@ -53,7 +62,7 @@ module Jekyll
53
62
  end
54
63
 
55
64
  def backup?(entry)
56
- entry[-1..-1] == "~"
65
+ entry.end_with?("~")
57
66
  end
58
67
 
59
68
  def excluded?(entry)
@@ -91,6 +100,7 @@ module Jekyll
91
100
  # Returns true if path matches against any glob pattern, else false.
92
101
  def glob_include?(enumerator, entry)
93
102
  entry_with_source = PathManager.join(site.source, entry)
103
+ entry_is_directory = File.directory?(entry_with_source)
94
104
 
95
105
  enumerator.any? do |pattern|
96
106
  case pattern
@@ -98,7 +108,8 @@ module Jekyll
98
108
  pattern_with_source = PathManager.join(site.source, pattern)
99
109
 
100
110
  File.fnmatch?(pattern_with_source, entry_with_source) ||
101
- entry_with_source.start_with?(pattern_with_source)
111
+ entry_with_source.start_with?(pattern_with_source) ||
112
+ (pattern_with_source == "#{entry_with_source}/" if entry_is_directory)
102
113
  when Regexp
103
114
  pattern.match?(entry_with_source)
104
115
  else
@@ -56,7 +56,7 @@ module Jekyll
56
56
  #
57
57
  # Returns true if the string passed in
58
58
  def include?(something)
59
- (output&.include?(something)) || content.include?(something)
59
+ output&.include?(something) || content.include?(something)
60
60
  end
61
61
 
62
62
  # The UID for this doc (useful in feeds).
@@ -113,7 +113,7 @@ module Jekyll
113
113
  #
114
114
  # Returns the formatted String
115
115
  def normalize_whitespace(input)
116
- input.to_s.gsub(%r!\s+!, " ").strip
116
+ input.to_s.gsub(%r!\s+!, " ").tap(&:strip!)
117
117
  end
118
118
 
119
119
  # Count the number of words in the input string.
@@ -121,8 +121,20 @@ module Jekyll
121
121
  # input - The String on which to operate.
122
122
  #
123
123
  # Returns the Integer word count.
124
- def number_of_words(input)
125
- input.split.length
124
+ def number_of_words(input, mode = nil)
125
+ cjk_charset = '\p{Han}\p{Katakana}\p{Hiragana}\p{Hangul}'
126
+ cjk_regex = %r![#{cjk_charset}]!o
127
+ word_regex = %r![^#{cjk_charset}\s]+!o
128
+
129
+ case mode
130
+ when "cjk"
131
+ input.scan(cjk_regex).length + input.scan(word_regex).length
132
+ when "auto"
133
+ cjk_count = input.scan(cjk_regex).length
134
+ cjk_count.zero? ? input.split.length : cjk_count + input.scan(word_regex).length
135
+ else
136
+ input.split.length
137
+ end
126
138
  end
127
139
 
128
140
  # Join an array of things into a string by separating with commas and the
@@ -210,6 +222,66 @@ module Jekyll
210
222
  end || []
211
223
  end
212
224
 
225
+ # Search an array of objects and returns the first object that has the queried attribute
226
+ # with the given value or returns nil otherwise.
227
+ #
228
+ # input - the object array.
229
+ # property - the property within each object to search by.
230
+ # value - the desired value.
231
+ # Cannot be an instance of Array nor Hash since calling #to_s on them returns
232
+ # their `#inspect` string object.
233
+ #
234
+ # Returns the found object or nil
235
+ #
236
+ # rubocop:disable Metrics/CyclomaticComplexity
237
+ def find(input, property, value)
238
+ return input if !property || value.is_a?(Array) || value.is_a?(Hash)
239
+ return input unless input.respond_to?(:find)
240
+
241
+ input = input.values if input.is_a?(Hash)
242
+ input_id = input.hash
243
+
244
+ # implement a hash based on method parameters to cache the end-result for given parameters.
245
+ @find_filter_cache ||= {}
246
+ @find_filter_cache[input_id] ||= {}
247
+ @find_filter_cache[input_id][property] ||= {}
248
+
249
+ # stash or retrive results to return
250
+ # Since `enum.find` can return nil or false, we use a placeholder string "<__NO MATCH__>"
251
+ # to validate caching.
252
+ result = @find_filter_cache[input_id][property][value] ||= begin
253
+ input.find do |object|
254
+ compare_property_vs_target(item_property(object, property), value)
255
+ end || "<__NO MATCH__>"
256
+ end
257
+ return nil if result == "<__NO MATCH__>"
258
+
259
+ result
260
+ end
261
+ # rubocop:enable Metrics/CyclomaticComplexity
262
+
263
+ # Searches an array of objects against an expression and returns the first object for which
264
+ # the expression evaluates to true, or returns nil otherwise.
265
+ #
266
+ # input - the object array
267
+ # variable - the variable to assign each item to in the expression
268
+ # expression - a Liquid comparison expression passed in as a string
269
+ #
270
+ # Returns the found object or nil
271
+ def find_exp(input, variable, expression)
272
+ return input unless input.respond_to?(:find)
273
+
274
+ input = input.values if input.is_a?(Hash)
275
+
276
+ condition = parse_condition(expression)
277
+ @context.stack do
278
+ input.find do |object|
279
+ @context[variable] = object
280
+ condition.evaluate(@context)
281
+ end
282
+ end
283
+ end
284
+
213
285
  # Convert the input into integer
214
286
  #
215
287
  # input - the object string
@@ -235,9 +307,10 @@ module Jekyll
235
307
  if property.nil?
236
308
  input.sort
237
309
  else
238
- if nils == "first"
310
+ case nils
311
+ when "first"
239
312
  order = - 1
240
- elsif nils == "last"
313
+ when "last"
241
314
  order = + 1
242
315
  else
243
316
  raise ArgumentError, "Invalid nils order: " \
@@ -327,8 +400,6 @@ module Jekyll
327
400
 
328
401
  # `where` filter helper
329
402
  #
330
- # rubocop:disable Metrics/CyclomaticComplexity
331
- # rubocop:disable Metrics/PerceivedComplexity
332
403
  def compare_property_vs_target(property, target)
333
404
  case target
334
405
  when NilClass
@@ -349,40 +420,49 @@ module Jekyll
349
420
 
350
421
  false
351
422
  end
352
- # rubocop:enable Metrics/CyclomaticComplexity
353
- # rubocop:enable Metrics/PerceivedComplexity
354
423
 
355
424
  def item_property(item, property)
356
- @item_property_cache ||= {}
425
+ @item_property_cache ||= @context.registers[:site].filter_cache[:item_property] ||= {}
357
426
  @item_property_cache[property] ||= {}
358
427
  @item_property_cache[property][item] ||= begin
359
- if item.respond_to?(:to_liquid)
360
- property.to_s.split(".").reduce(item.to_liquid) do |subvalue, attribute|
361
- parse_sort_input(subvalue[attribute])
362
- end
363
- elsif item.respond_to?(:data)
364
- parse_sort_input(item.data[property.to_s])
365
- else
366
- parse_sort_input(item[property.to_s])
367
- end
428
+ property = property.to_s
429
+ property = if item.respond_to?(:to_liquid)
430
+ read_liquid_attribute(item.to_liquid, property)
431
+ elsif item.respond_to?(:data)
432
+ item.data[property]
433
+ else
434
+ item[property]
435
+ end
436
+
437
+ parse_sort_input(property)
438
+ end
439
+ end
440
+
441
+ def read_liquid_attribute(liquid_data, property)
442
+ return liquid_data[property] unless property.include?(".")
443
+
444
+ property.split(".").reduce(liquid_data) do |data, key|
445
+ data.respond_to?(:[]) && data[key]
368
446
  end
369
447
  end
370
448
 
371
- # rubocop:disable Performance/RegexpMatch
449
+ FLOAT_LIKE = %r!\A\s*-?(?:\d+\.?\d*|\.\d+)\s*\Z!.freeze
450
+ INTEGER_LIKE = %r!\A\s*-?\d+\s*\Z!.freeze
451
+ private_constant :FLOAT_LIKE, :INTEGER_LIKE
452
+
372
453
  # return numeric values as numbers for proper sorting
373
454
  def parse_sort_input(property)
374
- number_like = %r!\A\s*-?(?:\d+\.?\d*|\.\d+)\s*\Z!
375
- return property.to_f if property =~ number_like
455
+ stringified = property.to_s
456
+ return property.to_i if INTEGER_LIKE.match?(stringified)
457
+ return property.to_f if FLOAT_LIKE.match?(stringified)
376
458
 
377
459
  property
378
460
  end
379
- # rubocop:enable Performance/RegexpMatch
380
461
 
381
462
  def as_liquid(item)
382
463
  case item
383
464
  when Hash
384
- pairs = item.map { |k, v| as_liquid([k, v]) }
385
- Hash[pairs]
465
+ item.each_with_object({}) { |(k, v), result| result[as_liquid(k)] = as_liquid(v) }
386
466
  when Array
387
467
  item.map { |i| as_liquid(i) }
388
468
  else
@@ -420,10 +500,14 @@ module Jekyll
420
500
  #
421
501
  # Returns an instance of Liquid::Condition
422
502
  def parse_binary_comparison(parser)
423
- parse_comparison(parser).tap do |condition|
424
- binary_operator = parser.id?("and") || parser.id?("or")
425
- condition.send(binary_operator, parse_comparison(parser)) if binary_operator
503
+ condition = parse_comparison(parser)
504
+ first_condition = condition
505
+ while (binary_operator = parser.id?("and") || parser.id?("or"))
506
+ child_condition = parse_comparison(parser)
507
+ condition.send(binary_operator, child_condition)
508
+ condition = child_condition
426
509
  end
510
+ first_condition
427
511
  end
428
512
 
429
513
  # Generates a Liquid::Condition object from a Liquid::Parser object based on whether the parsed