jekyll 3.9.3 → 4.4.1

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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +511 -89
  3. data/LICENSE +1 -1
  4. data/README.markdown +48 -27
  5. data/lib/blank_template/_config.yml +3 -0
  6. data/lib/blank_template/_layouts/default.html +12 -0
  7. data/lib/blank_template/_sass/base.scss +9 -0
  8. data/lib/blank_template/assets/css/main.scss +4 -0
  9. data/lib/blank_template/index.md +8 -0
  10. data/lib/jekyll/cache.rb +186 -0
  11. data/lib/jekyll/cleaner.rb +8 -7
  12. data/lib/jekyll/collection.rb +84 -11
  13. data/lib/jekyll/command.rb +33 -6
  14. data/lib/jekyll/commands/build.rb +8 -28
  15. data/lib/jekyll/commands/clean.rb +3 -2
  16. data/lib/jekyll/commands/doctor.rb +46 -35
  17. data/lib/jekyll/commands/help.rb +1 -1
  18. data/lib/jekyll/commands/new.rb +44 -50
  19. data/lib/jekyll/commands/new_theme.rb +27 -28
  20. data/lib/jekyll/commands/serve/live_reload_reactor.rb +9 -16
  21. data/lib/jekyll/commands/serve/servlet.rb +21 -22
  22. data/lib/jekyll/commands/serve/websockets.rb +1 -1
  23. data/lib/jekyll/commands/serve.rb +75 -97
  24. data/lib/jekyll/configuration.rb +66 -158
  25. data/lib/jekyll/converters/identity.rb +18 -0
  26. data/lib/jekyll/converters/markdown/kramdown_parser.rb +83 -33
  27. data/lib/jekyll/converters/markdown.rb +49 -40
  28. data/lib/jekyll/converters/smartypants.rb +34 -14
  29. data/lib/jekyll/convertible.rb +36 -34
  30. data/lib/jekyll/deprecator.rb +2 -4
  31. data/lib/jekyll/document.rb +107 -72
  32. data/lib/jekyll/drops/collection_drop.rb +3 -4
  33. data/lib/jekyll/drops/document_drop.rb +9 -3
  34. data/lib/jekyll/drops/drop.rb +115 -33
  35. data/lib/jekyll/drops/excerpt_drop.rb +8 -0
  36. data/lib/jekyll/drops/site_drop.rb +9 -8
  37. data/lib/jekyll/drops/static_file_drop.rb +4 -4
  38. data/lib/jekyll/drops/theme_drop.rb +39 -0
  39. data/lib/jekyll/drops/unified_payload_drop.rb +7 -2
  40. data/lib/jekyll/drops/url_drop.rb +55 -3
  41. data/lib/jekyll/entry_filter.rb +42 -51
  42. data/lib/jekyll/excerpt.rb +48 -38
  43. data/lib/jekyll/external.rb +20 -19
  44. data/lib/jekyll/filters/date_filters.rb +6 -3
  45. data/lib/jekyll/filters/grouping_filters.rb +1 -2
  46. data/lib/jekyll/filters/url_filters.rb +50 -15
  47. data/lib/jekyll/filters.rb +211 -50
  48. data/lib/jekyll/frontmatter_defaults.rb +45 -36
  49. data/lib/jekyll/hooks.rb +26 -26
  50. data/lib/jekyll/inclusion.rb +32 -0
  51. data/lib/jekyll/layout.rb +12 -19
  52. data/lib/jekyll/liquid_extensions.rb +0 -2
  53. data/lib/jekyll/liquid_renderer/file.rb +24 -3
  54. data/lib/jekyll/liquid_renderer/table.rb +26 -77
  55. data/lib/jekyll/liquid_renderer.rb +31 -16
  56. data/lib/jekyll/log_adapter.rb +5 -1
  57. data/lib/jekyll/page.rb +51 -23
  58. data/lib/jekyll/page_excerpt.rb +25 -0
  59. data/lib/jekyll/page_without_a_file.rb +0 -4
  60. data/lib/jekyll/path_manager.rb +74 -0
  61. data/lib/jekyll/plugin.rb +5 -11
  62. data/lib/jekyll/plugin_manager.rb +15 -5
  63. data/lib/jekyll/profiler.rb +51 -0
  64. data/lib/jekyll/reader.rb +65 -10
  65. data/lib/jekyll/readers/collection_reader.rb +1 -0
  66. data/lib/jekyll/readers/data_reader.rb +48 -10
  67. data/lib/jekyll/readers/layout_reader.rb +3 -12
  68. data/lib/jekyll/readers/page_reader.rb +5 -5
  69. data/lib/jekyll/readers/post_reader.rb +32 -19
  70. data/lib/jekyll/readers/static_file_reader.rb +4 -4
  71. data/lib/jekyll/readers/theme_assets_reader.rb +8 -5
  72. data/lib/jekyll/regenerator.rb +4 -12
  73. data/lib/jekyll/related_posts.rb +1 -1
  74. data/lib/jekyll/renderer.rb +34 -49
  75. data/lib/jekyll/site.rb +151 -58
  76. data/lib/jekyll/static_file.rb +64 -28
  77. data/lib/jekyll/stevenson.rb +4 -8
  78. data/lib/jekyll/tags/highlight.rb +44 -57
  79. data/lib/jekyll/tags/include.rb +114 -80
  80. data/lib/jekyll/tags/link.rb +12 -7
  81. data/lib/jekyll/tags/post_url.rb +33 -30
  82. data/lib/jekyll/theme.rb +20 -18
  83. data/lib/jekyll/theme_builder.rb +91 -89
  84. data/lib/jekyll/url.rb +18 -10
  85. data/lib/jekyll/utils/ansi.rb +2 -2
  86. data/lib/jekyll/utils/exec.rb +0 -1
  87. data/lib/jekyll/utils/internet.rb +2 -4
  88. data/lib/jekyll/utils/platforms.rb +37 -52
  89. data/lib/jekyll/utils/thread_event.rb +1 -5
  90. data/lib/jekyll/utils.rb +29 -28
  91. data/lib/jekyll/version.rb +1 -1
  92. data/lib/jekyll.rb +9 -14
  93. data/lib/site_template/.gitignore +2 -0
  94. data/lib/site_template/404.html +2 -1
  95. data/lib/site_template/_config.yml +17 -5
  96. data/lib/site_template/_posts/0000-00-00-welcome-to-jekyll.markdown.erb +5 -1
  97. data/lib/theme_template/README.md.erb +1 -3
  98. data/lib/theme_template/gitignore.erb +1 -0
  99. data/lib/theme_template/theme.gemspec.erb +1 -4
  100. data/rubocop/jekyll/assert_equal_literal_actual.rb +150 -0
  101. data/rubocop/jekyll/no_p_allowed.rb +5 -6
  102. data/rubocop/jekyll/no_puts_allowed.rb +5 -6
  103. metadata +149 -37
  104. data/lib/jekyll/converters/markdown/rdiscount_parser.rb +0 -37
  105. data/lib/jekyll/converters/markdown/redcarpet_parser.rb +0 -112
  106. data/lib/jekyll/utils/rouge.rb +0 -22
  107. /data/lib/site_template/{about.md → about.markdown} +0 -0
  108. /data/lib/site_template/{index.md → index.markdown} +0 -0
@@ -10,13 +10,17 @@ module Jekyll
10
10
  # Returns the absolute URL as a String.
11
11
  def absolute_url(input)
12
12
  return if input.nil?
13
- input = input.url if input.respond_to?(:url)
14
- return input if Addressable::URI.parse(input.to_s).absolute?
15
- site = @context.registers[:site]
16
- return relative_url(input) if site.config["url"].nil?
17
- Addressable::URI.parse(
18
- site.config["url"].to_s + relative_url(input)
19
- ).normalize.to_s
13
+
14
+ cache = if input.is_a?(String)
15
+ (@context.registers[:site].filter_cache[:absolute_url] ||= {})
16
+ else
17
+ (@context.registers[:cached_absolute_url] ||= {})
18
+ end
19
+ cache[input] ||= compute_absolute_url(input)
20
+
21
+ # Duplicate cached string so that the cached value is never mutated by
22
+ # a subsequent filter.
23
+ cache[input].dup
20
24
  end
21
25
 
22
26
  # Produces a URL relative to the domain root based on site.baseurl
@@ -27,13 +31,17 @@ module Jekyll
27
31
  # Returns a URL relative to the domain root as a String.
28
32
  def relative_url(input)
29
33
  return if input.nil?
30
- input = input.url if input.respond_to?(:url)
31
- return input if Addressable::URI.parse(input.to_s).absolute?
32
34
 
33
- parts = [sanitized_baseurl, input]
34
- Addressable::URI.parse(
35
- parts.compact.map { |part| ensure_leading_slash(part.to_s) }.join
36
- ).normalize.to_s
35
+ cache = if input.is_a?(String)
36
+ (@context.registers[:site].filter_cache[:relative_url] ||= {})
37
+ else
38
+ (@context.registers[:cached_relative_url] ||= {})
39
+ end
40
+ cache[input] ||= compute_relative_url(input)
41
+
42
+ # Duplicate cached string so that the cached value is never mutated by
43
+ # a subsequent filter.
44
+ cache[input].dup
37
45
  end
38
46
 
39
47
  # Strips trailing `/index.html` from URLs to create pretty permalinks
@@ -43,21 +51,48 @@ module Jekyll
43
51
  # Returns a URL with the trailing `/index.html` removed
44
52
  def strip_index(input)
45
53
  return if input.nil? || input.to_s.empty?
54
+
46
55
  input.sub(%r!/index\.html?$!, "/")
47
56
  end
48
57
 
49
58
  private
50
59
 
60
+ def compute_absolute_url(input)
61
+ input = input.url if input.respond_to?(:url)
62
+ return input if Addressable::URI.parse(input.to_s).absolute?
63
+
64
+ site = @context.registers[:site]
65
+ site_url = site.config["url"]
66
+ return relative_url(input) if site_url.nil? || site_url == ""
67
+
68
+ Addressable::URI.parse(
69
+ site_url.to_s + relative_url(input)
70
+ ).normalize.to_s
71
+ end
72
+
73
+ def compute_relative_url(input)
74
+ input = input.url if input.respond_to?(:url)
75
+ return input if Addressable::URI.parse(input.to_s).absolute?
76
+
77
+ parts = [sanitized_baseurl, input]
78
+ Addressable::URI.parse(
79
+ parts.map! { |part| ensure_leading_slash(part.to_s) }.join
80
+ ).normalize.to_s
81
+ end
82
+
51
83
  def sanitized_baseurl
52
84
  site = @context.registers[:site]
53
- site.config["baseurl"].to_s.chomp("/")
85
+ baseurl = site.config["baseurl"]
86
+ return "" if baseurl.nil?
87
+
88
+ baseurl.to_s.chomp("/")
54
89
  end
55
90
 
56
91
  def ensure_leading_slash(input)
57
92
  return input if input.nil? || input.empty? || input.start_with?("/")
93
+
58
94
  "/#{input}"
59
95
  end
60
-
61
96
  end
62
97
  end
63
98
  end
@@ -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
@@ -161,14 +173,17 @@ module Jekyll
161
173
 
162
174
  # Filter an array of objects
163
175
  #
164
- # input - the object array
165
- # property - property within each object to filter by
166
- # value - desired value
176
+ # input - the object array.
177
+ # property - the property within each object to filter by.
178
+ # value - the desired value.
179
+ # Cannot be an instance of Array nor Hash since calling #to_s on them returns
180
+ # their `#inspect` string object.
167
181
  #
168
182
  # Returns the filtered array of objects
169
183
  def where(input, property, value)
170
- return input if property.nil? || value.nil?
184
+ return input if !property || value.is_a?(Array) || value.is_a?(Hash)
171
185
  return input unless input.respond_to?(:select)
186
+
172
187
  input = input.values if input.is_a?(Hash)
173
188
  input_id = input.hash
174
189
 
@@ -178,12 +193,10 @@ module Jekyll
178
193
  @where_filter_cache[input_id] ||= {}
179
194
  @where_filter_cache[input_id][property] ||= {}
180
195
 
181
- # stash or retrive results to return
182
- @where_filter_cache[input_id][property][value] ||= begin
183
- input.select do |object|
184
- Array(item_property(object, property)).map!(&:to_s).include?(value.to_s)
185
- end || []
186
- end
196
+ # stash or retrieve results to return
197
+ @where_filter_cache[input_id][property][value] ||= input.select do |object|
198
+ compare_property_vs_target(item_property(object, property), value)
199
+ end.to_a
187
200
  end
188
201
 
189
202
  # Filters an array of objects against an expression
@@ -195,6 +208,7 @@ module Jekyll
195
208
  # Returns the filtered array of objects
196
209
  def where_exp(input, variable, expression)
197
210
  return input unless input.respond_to?(:select)
211
+
198
212
  input = input.values if input.is_a?(Hash) # FIXME
199
213
 
200
214
  condition = parse_condition(expression)
@@ -206,6 +220,65 @@ module Jekyll
206
220
  end || []
207
221
  end
208
222
 
223
+ # Search an array of objects and returns the first object that has the queried attribute
224
+ # with the given value or returns nil otherwise.
225
+ #
226
+ # input - the object array.
227
+ # property - the property within each object to search by.
228
+ # value - the desired value.
229
+ # Cannot be an instance of Array nor Hash since calling #to_s on them returns
230
+ # their `#inspect` string object.
231
+ #
232
+ # Returns the found object or nil
233
+ #
234
+ # rubocop:disable Metrics/CyclomaticComplexity
235
+ def find(input, property, value)
236
+ return input if !property || value.is_a?(Array) || value.is_a?(Hash)
237
+ return input unless input.respond_to?(:find)
238
+
239
+ input = input.values if input.is_a?(Hash)
240
+ input_id = input.hash
241
+
242
+ # implement a hash based on method parameters to cache the end-result for given parameters.
243
+ @find_filter_cache ||= {}
244
+ @find_filter_cache[input_id] ||= {}
245
+ @find_filter_cache[input_id][property] ||= {}
246
+
247
+ # stash or retrieve results to return
248
+ # Since `enum.find` can return nil or false, we use a placeholder string "<__NO MATCH__>"
249
+ # to validate caching.
250
+ result = @find_filter_cache[input_id][property][value] ||= input.find do |object|
251
+ compare_property_vs_target(item_property(object, property), value)
252
+ end || "<__NO MATCH__>"
253
+
254
+ return nil if result == "<__NO MATCH__>"
255
+
256
+ result
257
+ end
258
+ # rubocop:enable Metrics/CyclomaticComplexity
259
+
260
+ # Searches an array of objects against an expression and returns the first object for which
261
+ # the expression evaluates to true, or returns nil otherwise.
262
+ #
263
+ # input - the object array
264
+ # variable - the variable to assign each item to in the expression
265
+ # expression - a Liquid comparison expression passed in as a string
266
+ #
267
+ # Returns the found object or nil
268
+ def find_exp(input, variable, expression)
269
+ return input unless input.respond_to?(:find)
270
+
271
+ input = input.values if input.is_a?(Hash)
272
+
273
+ condition = parse_condition(expression)
274
+ @context.stack do
275
+ input.find do |object|
276
+ @context[variable] = object
277
+ condition.evaluate(@context)
278
+ end
279
+ end
280
+ end
281
+
209
282
  # Convert the input into integer
210
283
  #
211
284
  # input - the object string
@@ -214,6 +287,7 @@ module Jekyll
214
287
  def to_integer(input)
215
288
  return 1 if input == true
216
289
  return 0 if input == false
290
+
217
291
  input.to_i
218
292
  end
219
293
 
@@ -225,19 +299,19 @@ module Jekyll
225
299
  #
226
300
  # Returns the filtered array of objects
227
301
  def sort(input, property = nil, nils = "first")
228
- if input.nil?
229
- raise ArgumentError, "Cannot sort a null object."
230
- end
302
+ raise ArgumentError, "Cannot sort a null object." if input.nil?
303
+
231
304
  if property.nil?
232
305
  input.sort
233
306
  else
234
- if nils == "first"
307
+ case nils
308
+ when "first"
235
309
  order = - 1
236
- elsif nils == "last"
310
+ when "last"
237
311
  order = + 1
238
312
  else
239
313
  raise ArgumentError, "Invalid nils order: " \
240
- "'#{nils}' is not a valid nils order. It must be 'first' or 'last'."
314
+ "'#{nils}' is not a valid nils order. It must be 'first' or 'last'."
241
315
  end
242
316
 
243
317
  sort_input(input, property, order)
@@ -246,6 +320,7 @@ module Jekyll
246
320
 
247
321
  def pop(array, num = 1)
248
322
  return array unless array.is_a?(Array)
323
+
249
324
  num = Liquid::Utils.to_integer(num)
250
325
  new_ary = array.dup
251
326
  new_ary.pop(num)
@@ -254,6 +329,7 @@ module Jekyll
254
329
 
255
330
  def push(array, input)
256
331
  return array unless array.is_a?(Array)
332
+
257
333
  new_ary = array.dup
258
334
  new_ary.push(input)
259
335
  new_ary
@@ -261,6 +337,7 @@ module Jekyll
261
337
 
262
338
  def shift(array, num = 1)
263
339
  return array unless array.is_a?(Array)
340
+
264
341
  num = Liquid::Utils.to_integer(num)
265
342
  new_ary = array.dup
266
343
  new_ary.shift(num)
@@ -269,6 +346,7 @@ module Jekyll
269
346
 
270
347
  def unshift(array, input)
271
348
  return array unless array.is_a?(Array)
349
+
272
350
  new_ary = array.dup
273
351
  new_ary.unshift(input)
274
352
  new_ary
@@ -276,6 +354,7 @@ module Jekyll
276
354
 
277
355
  def sample(input, num = 1)
278
356
  return input unless input.respond_to?(:sample)
357
+
279
358
  num = Liquid::Utils.to_integer(num) rescue 1
280
359
  if num == 1
281
360
  input.sample
@@ -301,40 +380,94 @@ module Jekyll
301
380
  # We also utilize the Schwartzian transform to make this more efficient.
302
381
  def sort_input(input, property, order)
303
382
  input.map { |item| [item_property(item, property), item] }
304
- .sort! do |apple_info, orange_info|
305
- apple_property = apple_info.first
306
- orange_property = orange_info.first
383
+ .sort! do |a_info, b_info|
384
+ a_property = a_info.first
385
+ b_property = b_info.first
307
386
 
308
- if !apple_property.nil? && orange_property.nil?
387
+ if !a_property.nil? && b_property.nil?
309
388
  - order
310
- elsif apple_property.nil? && !orange_property.nil?
389
+ elsif a_property.nil? && !b_property.nil?
311
390
  + order
312
391
  else
313
- apple_property <=> orange_property
392
+ a_property <=> b_property || a_property.to_s <=> b_property.to_s
314
393
  end
315
394
  end
316
395
  .map!(&:last)
317
396
  end
318
397
 
319
- private
320
- def item_property(item, property)
321
- if item.respond_to?(:to_liquid)
322
- property.to_s.split(".").reduce(item.to_liquid) do |subvalue, attribute|
323
- subvalue[attribute]
324
- end
325
- elsif item.respond_to?(:data)
326
- item.data[property.to_s]
398
+ # `where` filter helper
399
+ #
400
+ def compare_property_vs_target(property, target)
401
+ case target
402
+ when NilClass
403
+ return true if property.nil?
404
+ when Liquid::Expression::MethodLiteral # `empty` or `blank`
405
+ target = target.to_s
406
+ return true if property == target || Array(property).join == target
327
407
  else
328
- item[property.to_s]
408
+ target = target.to_s
409
+ if property.is_a? String
410
+ return true if property == target
411
+ else
412
+ Array(property).each do |prop|
413
+ return true if prop.to_s == target
414
+ end
415
+ end
329
416
  end
417
+
418
+ false
419
+ end
420
+
421
+ def item_property(item, property)
422
+ @item_property_cache ||= @context.registers[:site].filter_cache[:item_property] ||= {}
423
+ @item_property_cache[property] ||= {}
424
+ @item_property_cache[property][item] ||= begin
425
+ property = property.to_s
426
+ property = if item.respond_to?(:to_liquid)
427
+ read_liquid_attribute(item.to_liquid, property)
428
+ elsif item.respond_to?(:data)
429
+ item.data[property]
430
+ else
431
+ item[property]
432
+ end
433
+
434
+ parse_sort_input(property)
435
+ end
436
+ end
437
+
438
+ def read_liquid_attribute(liquid_data, property)
439
+ return liquid_data[property] unless property.include?(".")
440
+
441
+ property.split(".").reduce(liquid_data) do |data, key|
442
+ data.respond_to?(:[]) && data[key]
443
+ end
444
+ rescue TypeError => e
445
+ msg = if liquid_data.is_a?(Array)
446
+ "Error accessing object (#{liquid_data.to_s[0...20]}) with given key. Expected an " \
447
+ "integer but got #{property.inspect} instead."
448
+ else
449
+ e.message
450
+ end
451
+ raise e, msg
452
+ end
453
+
454
+ FLOAT_LIKE = %r!\A\s*-?(?:\d+\.?\d*|\.\d+)\s*\Z!.freeze
455
+ INTEGER_LIKE = %r!\A\s*-?\d+\s*\Z!.freeze
456
+ private_constant :FLOAT_LIKE, :INTEGER_LIKE
457
+
458
+ # return numeric values as numbers for proper sorting
459
+ def parse_sort_input(property)
460
+ stringified = property.to_s
461
+ return property.to_i if INTEGER_LIKE.match?(stringified)
462
+ return property.to_f if FLOAT_LIKE.match?(stringified)
463
+
464
+ property
330
465
  end
331
466
 
332
- private
333
467
  def as_liquid(item)
334
468
  case item
335
469
  when Hash
336
- pairs = item.map { |k, v| as_liquid([k, v]) }
337
- Hash[pairs]
470
+ item.each_with_object({}) { |(k, v), result| result[as_liquid(k)] = as_liquid(v) }
338
471
  when Array
339
472
  item.map { |i| as_liquid(i) }
340
473
  else
@@ -352,25 +485,53 @@ module Jekyll
352
485
  end
353
486
  end
354
487
 
488
+ # ----------- The following set of code was *adapted* from Liquid::If
489
+ # ----------- ref: https://github.com/Shopify/liquid/blob/ffb0ace30315bbcf3548a0383fab531452060ae8/lib/liquid/tags/if.rb#L84-L107
490
+
355
491
  # Parse a string to a Liquid Condition
356
- private
357
492
  def parse_condition(exp)
358
- parser = Liquid::Parser.new(exp)
359
- left_expr = parser.expression
360
- operator = parser.consume?(:comparison)
361
- condition =
362
- if operator
363
- Liquid::Condition.new(Liquid::Expression.parse(left_expr),
364
- operator,
365
- Liquid::Expression.parse(parser.expression))
366
- else
367
- Liquid::Condition.new(Liquid::Expression.parse(left_expr))
368
- end
369
- parser.consume(:end_of_string)
493
+ parser = Liquid::Parser.new(exp)
494
+ condition = parse_binary_comparison(parser)
370
495
 
496
+ parser.consume(:end_of_string)
371
497
  condition
372
498
  end
373
499
 
500
+ # Generate a Liquid::Condition object from a Liquid::Parser object additionally processing
501
+ # the parsed expression based on whether the expression consists of binary operations with
502
+ # Liquid operators `and` or `or`
503
+ #
504
+ # - parser: an instance of Liquid::Parser
505
+ #
506
+ # Returns an instance of Liquid::Condition
507
+ def parse_binary_comparison(parser)
508
+ condition = parse_comparison(parser)
509
+ first_condition = condition
510
+ while (binary_operator = parser.id?("and") || parser.id?("or"))
511
+ child_condition = parse_comparison(parser)
512
+ condition.send(binary_operator, child_condition)
513
+ condition = child_condition
514
+ end
515
+ first_condition
516
+ end
517
+
518
+ # Generates a Liquid::Condition object from a Liquid::Parser object based on whether the parsed
519
+ # expression involves a "comparison" operator (e.g. <, ==, >, !=, etc)
520
+ #
521
+ # - parser: an instance of Liquid::Parser
522
+ #
523
+ # Returns an instance of Liquid::Condition
524
+ def parse_comparison(parser)
525
+ left_operand = Liquid::Expression.parse(parser.expression)
526
+ operator = parser.consume?(:comparison)
527
+
528
+ # No comparison-operator detected. Initialize a Liquid::Condition using only left operand
529
+ return Liquid::Condition.new(left_operand) unless operator
530
+
531
+ # Parse what remained after extracting the left operand and the `:comparison` operator
532
+ # and initialize a Liquid::Condition object using the operands and the comparison-operator
533
+ Liquid::Condition.new(left_operand, operator, Liquid::Expression.parse(parser.expression))
534
+ end
374
535
  end
375
536
  end
376
537
 
@@ -12,6 +12,10 @@ module Jekyll
12
12
  @site = site
13
13
  end
14
14
 
15
+ def reset
16
+ @glob_cache = {} if @glob_cache
17
+ end
18
+
15
19
  def update_deprecated_types(set)
16
20
  return set unless set.key?("scope") && set["scope"].key?("type")
17
21
 
@@ -36,6 +40,7 @@ module Jekyll
36
40
  def ensure_time!(set)
37
41
  return set unless set.key?("values") && set["values"].key?("date")
38
42
  return set if set["values"]["date"].is_a?(Time)
43
+
39
44
  set["values"]["date"] = Utils.parse_date(
40
45
  set["values"]["date"],
41
46
  "An invalid date format was found in a front-matter default set: #{set}"
@@ -92,48 +97,51 @@ module Jekyll
92
97
  # path - the path to check for
93
98
  # type - the type (:post, :page or :draft) to check for
94
99
  #
95
- # Returns true if the scope applies to the given path and type
100
+ # Returns true if the scope applies to the given type and path
96
101
  def applies?(scope, path, type)
97
- applies_path?(scope, path) && applies_type?(scope, type)
102
+ applies_type?(scope, type) && applies_path?(scope, path)
98
103
  end
99
104
 
100
- # rubocop:disable Metrics/AbcSize
101
105
  def applies_path?(scope, path)
102
- return true if !scope.key?("path") || scope["path"].empty?
103
-
104
- sanitized_path = Pathname.new(sanitize_path(path))
105
- site_path = Pathname.new(@site.source)
106
- rel_scope_path = Pathname.new(scope["path"])
107
- abs_scope_path = File.join(@site.source, rel_scope_path)
108
-
109
- if scope["path"].to_s.include?("*")
110
- Dir.glob(abs_scope_path).each do |scope_path|
111
- scope_path = Pathname.new(scope_path).relative_path_from(site_path)
112
- scope_path = strip_collections_dir(scope_path)
113
- Jekyll.logger.debug "Globbed Scope Path:", scope_path
114
- return true if path_is_subpath?(sanitized_path, scope_path)
115
- end
116
- false
106
+ rel_scope_path = scope["path"]
107
+ return true if !rel_scope_path.is_a?(String) || rel_scope_path.empty?
108
+
109
+ sanitized_path = sanitize_path(path)
110
+
111
+ if rel_scope_path.include?("*")
112
+ glob_scope(sanitized_path, rel_scope_path)
117
113
  else
118
114
  path_is_subpath?(sanitized_path, strip_collections_dir(rel_scope_path))
119
115
  end
120
116
  end
121
- # rubocop:enable Metrics/AbcSize
122
117
 
123
- def path_is_subpath?(path, parent_path)
124
- path.ascend do |ascended_path|
125
- if ascended_path.to_s == parent_path.to_s
126
- return true
127
- end
128
- end
118
+ def glob_scope(sanitized_path, rel_scope_path)
119
+ site_source = Pathname.new(@site.source)
120
+ abs_scope_path = site_source.join(rel_scope_path).to_s
129
121
 
122
+ glob_cache(abs_scope_path).each do |scope_path|
123
+ scope_path = Pathname.new(scope_path).relative_path_from(site_source).to_s
124
+ scope_path = strip_collections_dir(scope_path)
125
+ Jekyll.logger.debug "Globbed Scope Path:", scope_path
126
+ return true if path_is_subpath?(sanitized_path, scope_path)
127
+ end
130
128
  false
131
129
  end
132
130
 
131
+ def glob_cache(path)
132
+ @glob_cache ||= {}
133
+ @glob_cache[path] ||= Dir.glob(path)
134
+ end
135
+
136
+ def path_is_subpath?(path, parent_path)
137
+ path.start_with?(parent_path)
138
+ end
139
+
133
140
  def strip_collections_dir(path)
134
141
  collections_dir = @site.config["collections_dir"]
135
- slashed_coll_dir = "#{collections_dir}/"
142
+ slashed_coll_dir = collections_dir.empty? ? "/" : "#{collections_dir}/"
136
143
  return path if collections_dir.empty? || !path.to_s.start_with?(slashed_coll_dir)
144
+
137
145
  path.sub(slashed_coll_dir, "")
138
146
  end
139
147
 
@@ -149,7 +157,7 @@ module Jekyll
149
157
  # Returns true if either of the above conditions are satisfied,
150
158
  # otherwise returns false
151
159
  def applies_type?(scope, type)
152
- !scope.key?("type") || scope["type"].eql?(type.to_s)
160
+ !scope.key?("type") || type&.to_sym.eql?(scope["type"].to_sym)
153
161
  end
154
162
 
155
163
  # Checks if a given set of default values is valid
@@ -167,7 +175,7 @@ module Jekyll
167
175
  # new_scope - the new scope hash
168
176
  #
169
177
  # Returns true if the new scope has precedence over the older
170
- # rubocop: disable PredicateName
178
+ # rubocop: disable Naming/PredicateName
171
179
  def has_precedence?(old_scope, new_scope)
172
180
  return true if old_scope.nil?
173
181
 
@@ -182,13 +190,15 @@ module Jekyll
182
190
  !old_scope.key? "type"
183
191
  end
184
192
  end
185
- # rubocop: enable PredicateName
193
+ # rubocop: enable Naming/PredicateName
186
194
 
187
195
  # Collects a list of sets that match the given path and type
188
196
  #
189
197
  # Returns an array of hashes
190
198
  def matching_sets(path, type)
191
- valid_sets.select do |set|
199
+ @matched_set_cache ||= {}
200
+ @matched_set_cache[path] ||= {}
201
+ @matched_set_cache[path][type] ||= valid_sets.select do |set|
192
202
  !set.key?("scope") || applies?(set["scope"], path, type)
193
203
  end
194
204
  end
@@ -211,18 +221,17 @@ module Jekyll
211
221
  Jekyll.logger.warn set.to_s
212
222
  nil
213
223
  end
214
- end.compact
224
+ end.tap(&:compact!)
215
225
  end
216
226
 
217
- # Sanitizes the given path by removing a leading and adding a trailing slash
218
-
219
- SANITIZATION_REGEX = %r!\A/|(?<=[^/])\z!
220
-
227
+ # Sanitizes the given path by removing a leading slash
221
228
  def sanitize_path(path)
222
229
  if path.nil? || path.empty?
223
230
  ""
231
+ elsif path.start_with?("/")
232
+ path.gsub(%r!\A/|(?<=[^/])\z!, "")
224
233
  else
225
- path.gsub(SANITIZATION_REGEX, "")
234
+ path
226
235
  end
227
236
  end
228
237
  end