jekyll 4.0.0.pre.beta1 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
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