jekyll 4.0.1 → 4.1.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +48 -19
  3. data/lib/jekyll.rb +3 -0
  4. data/lib/jekyll/collection.rb +1 -1
  5. data/lib/jekyll/command.rb +4 -2
  6. data/lib/jekyll/commands/new.rb +2 -2
  7. data/lib/jekyll/commands/serve.rb +9 -1
  8. data/lib/jekyll/configuration.rb +1 -1
  9. data/lib/jekyll/converters/identity.rb +2 -2
  10. data/lib/jekyll/converters/markdown/kramdown_parser.rb +70 -1
  11. data/lib/jekyll/convertible.rb +15 -15
  12. data/lib/jekyll/document.rb +18 -4
  13. data/lib/jekyll/drops/document_drop.rb +12 -0
  14. data/lib/jekyll/drops/page_drop.rb +18 -0
  15. data/lib/jekyll/drops/url_drop.rb +8 -0
  16. data/lib/jekyll/entry_filter.rb +19 -6
  17. data/lib/jekyll/excerpt.rb +1 -1
  18. data/lib/jekyll/filters.rb +99 -14
  19. data/lib/jekyll/filters/url_filters.rb +41 -14
  20. data/lib/jekyll/frontmatter_defaults.rb +12 -17
  21. data/lib/jekyll/hooks.rb +2 -5
  22. data/lib/jekyll/inclusion.rb +32 -0
  23. data/lib/jekyll/liquid_renderer.rb +18 -15
  24. data/lib/jekyll/liquid_renderer/table.rb +1 -21
  25. data/lib/jekyll/page.rb +43 -0
  26. data/lib/jekyll/page_excerpt.rb +26 -0
  27. data/lib/jekyll/profiler.rb +58 -0
  28. data/lib/jekyll/readers/collection_reader.rb +1 -0
  29. data/lib/jekyll/readers/data_reader.rb +1 -0
  30. data/lib/jekyll/readers/layout_reader.rb +1 -0
  31. data/lib/jekyll/readers/page_reader.rb +1 -0
  32. data/lib/jekyll/readers/post_reader.rb +1 -0
  33. data/lib/jekyll/readers/static_file_reader.rb +1 -0
  34. data/lib/jekyll/readers/theme_assets_reader.rb +1 -0
  35. data/lib/jekyll/renderer.rb +9 -15
  36. data/lib/jekyll/site.rb +14 -5
  37. data/lib/jekyll/static_file.rb +14 -9
  38. data/lib/jekyll/tags/include.rb +58 -3
  39. data/lib/jekyll/theme.rb +6 -0
  40. data/lib/jekyll/utils.rb +4 -4
  41. data/lib/jekyll/utils/win_tz.rb +1 -1
  42. data/lib/jekyll/version.rb +1 -1
  43. data/lib/theme_template/theme.gemspec.erb +1 -4
  44. metadata +14 -31
@@ -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.
@@ -414,9 +418,13 @@ module Jekyll
414
418
  #
415
419
  # Returns nothing.
416
420
  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 }
421
+ if relative_path.start_with?(special_dir)
422
+ superdirs = []
423
+ else
424
+ superdirs = relative_path.sub(Document.superdirs_regex(special_dir), "")
425
+ superdirs = superdirs.split(File::SEPARATOR)
426
+ superdirs.reject! { |c| c.empty? || c == special_dir || c == basename }
427
+ end
420
428
 
421
429
  merge_data!({ "categories" => superdirs }, :source => "file path")
422
430
  end
@@ -490,6 +498,7 @@ module Jekyll
490
498
  end
491
499
  end
492
500
 
501
+ # rubocop:disable Metrics/AbcSize
493
502
  def populate_title
494
503
  if relative_path =~ DATE_FILENAME_MATCHER
495
504
  date, slug, ext = Regexp.last_match.captures
@@ -497,6 +506,10 @@ module Jekyll
497
506
  elsif relative_path =~ DATELESS_FILENAME_MATCHER
498
507
  slug, ext = Regexp.last_match.captures
499
508
  end
509
+ # `slug` will be nil for documents without an extension since the regex patterns
510
+ # above tests for an extension as well.
511
+ # In such cases, assign `basename_without_ext` as the slug.
512
+ slug ||= basename_without_ext
500
513
 
501
514
  # slugs shouldn't end with a period
502
515
  # `String#gsub!` removes all trailing periods (in comparison to `String#chomp!`)
@@ -508,6 +521,7 @@ module Jekyll
508
521
  data["slug"] ||= slug
509
522
  data["ext"] ||= ext
510
523
  end
524
+ # rubocop:enable Metrics/AbcSize
511
525
 
512
526
  def modify_date(date)
513
527
  if !data["date"] || data["date"].to_i == site.time.to_i
@@ -64,6 +64,18 @@ module Jekyll
64
64
  result[key] = doc[key] unless NESTED_OBJECT_FIELD_BLACKLIST.include?(key)
65
65
  end
66
66
  end
67
+
68
+ def title
69
+ @obj.data["title"]
70
+ end
71
+
72
+ def categories
73
+ @obj.data["categories"]
74
+ end
75
+
76
+ def tags
77
+ @obj.data["tags"]
78
+ end
67
79
  end
68
80
  end
69
81
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Drops
5
+ class PageDrop < Drop
6
+ extend Forwardable
7
+
8
+ mutable false
9
+
10
+ def_delegators :@obj, :content, :dir, :name, :path, :url, :excerpt
11
+ private def_delegator :@obj, :data, :fallback_data
12
+
13
+ def title
14
+ @obj.data["title"]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -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")
@@ -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)
@@ -27,20 +28,30 @@ module Jekyll
27
28
  )
28
29
  end
29
30
 
31
+ # rubocop:disable Metrics/CyclomaticComplexity
30
32
  def filter(entries)
31
33
  entries.reject do |e|
32
34
  # Reject this entry if it is just a "dot" representation.
33
35
  # e.g.: '.', '..', '_movies/.', 'music/..', etc
34
36
  next true if e.end_with?(".")
35
- # Reject this entry if it is a symlink.
37
+
38
+ # Check if the current entry is explicitly included and cache the result
39
+ included = included?(e)
40
+
41
+ # Reject current entry if it is excluded but not explicitly included as well.
42
+ next true if excluded?(e) && !included
43
+
44
+ # Reject current entry if it is a symlink.
36
45
  next true if symlink?(e)
37
- # Do not reject this entry if it is included.
38
- next false if included?(e)
39
46
 
40
- # Reject this entry if it is special, a backup file, or excluded.
41
- special?(e) || backup?(e) || excluded?(e)
47
+ # Do not reject current entry if it is explicitly included.
48
+ next false if included
49
+
50
+ # Reject current entry if it is special or a backup file.
51
+ special?(e) || backup?(e)
42
52
  end
43
53
  end
54
+ # rubocop:enable Metrics/CyclomaticComplexity
44
55
 
45
56
  def included?(entry)
46
57
  glob_include?(site.include, entry) ||
@@ -91,6 +102,7 @@ module Jekyll
91
102
  # Returns true if path matches against any glob pattern, else false.
92
103
  def glob_include?(enumerator, entry)
93
104
  entry_with_source = PathManager.join(site.source, entry)
105
+ entry_is_directory = File.directory?(entry_with_source)
94
106
 
95
107
  enumerator.any? do |pattern|
96
108
  case pattern
@@ -98,7 +110,8 @@ module Jekyll
98
110
  pattern_with_source = PathManager.join(site.source, pattern)
99
111
 
100
112
  File.fnmatch?(pattern_with_source, entry_with_source) ||
101
- entry_with_source.start_with?(pattern_with_source)
113
+ entry_with_source.start_with?(pattern_with_source) ||
114
+ (pattern_with_source == "#{entry_with_source}/" if entry_is_directory)
102
115
  when Regexp
103
116
  pattern.match?(entry_with_source)
104
117
  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).
@@ -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
@@ -356,15 +428,24 @@ module Jekyll
356
428
  @item_property_cache ||= {}
357
429
  @item_property_cache[property] ||= {}
358
430
  @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
431
+ property = property.to_s
432
+ property = if item.respond_to?(:to_liquid)
433
+ read_liquid_attribute(item.to_liquid, property)
434
+ elsif item.respond_to?(:data)
435
+ item.data[property]
436
+ else
437
+ item[property]
438
+ end
439
+
440
+ parse_sort_input(property)
441
+ end
442
+ end
443
+
444
+ def read_liquid_attribute(liquid_data, property)
445
+ return liquid_data[property] unless property.include?(".")
446
+
447
+ property.split(".").reduce(liquid_data) do |data, key|
448
+ data.respond_to?(:[]) && data[key]
368
449
  end
369
450
  end
370
451
 
@@ -423,10 +504,14 @@ module Jekyll
423
504
  #
424
505
  # Returns an instance of Liquid::Condition
425
506
  def parse_binary_comparison(parser)
426
- parse_comparison(parser).tap do |condition|
427
- binary_operator = parser.id?("and") || parser.id?("or")
428
- condition.send(binary_operator, parse_comparison(parser)) if binary_operator
507
+ condition = parse_comparison(parser)
508
+ first_condition = condition
509
+ while (binary_operator = parser.id?("and") || parser.id?("or"))
510
+ child_condition = parse_comparison(parser)
511
+ condition.send(binary_operator, child_condition)
512
+ condition = child_condition
429
513
  end
514
+ first_condition
430
515
  end
431
516
 
432
517
  # Generates a Liquid::Condition object from a Liquid::Parser object based on whether the parsed
@@ -11,15 +11,16 @@ module Jekyll
11
11
  def absolute_url(input)
12
12
  return if input.nil?
13
13
 
14
- input = input.url if input.respond_to?(:url)
15
- return input if Addressable::URI.parse(input.to_s).absolute?
16
-
17
- site = @context.registers[:site]
18
- return relative_url(input) if site.config["url"].nil?
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)
19
20
 
20
- Addressable::URI.parse(
21
- site.config["url"].to_s + relative_url(input)
22
- ).normalize.to_s
21
+ # Duplicate cached string so that the cached value is never mutated by
22
+ # a subsequent filter.
23
+ cache[input].dup
23
24
  end
24
25
 
25
26
  # Produces a URL relative to the domain root based on site.baseurl
@@ -31,13 +32,16 @@ module Jekyll
31
32
  def relative_url(input)
32
33
  return if input.nil?
33
34
 
34
- input = input.url if input.respond_to?(:url)
35
- return input if Addressable::URI.parse(input.to_s).absolute?
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)
36
41
 
37
- parts = [sanitized_baseurl, input]
38
- Addressable::URI.parse(
39
- parts.compact.map { |part| ensure_leading_slash(part.to_s) }.join
40
- ).normalize.to_s
42
+ # Duplicate cached string so that the cached value is never mutated by
43
+ # a subsequent filter.
44
+ cache[input].dup
41
45
  end
42
46
 
43
47
  # Strips trailing `/index.html` from URLs to create pretty permalinks
@@ -53,6 +57,29 @@ module Jekyll
53
57
 
54
58
  private
55
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.compact.map { |part| ensure_leading_slash(part.to_s) }.join
80
+ ).normalize.to_s
81
+ end
82
+
56
83
  def sanitized_baseurl
57
84
  site = @context.registers[:site]
58
85
  site.config["baseurl"].to_s.chomp("/")
@@ -103,15 +103,15 @@ module Jekyll
103
103
  end
104
104
 
105
105
  def applies_path?(scope, path)
106
- return true if !scope.key?("path") || scope["path"].empty?
106
+ rel_scope_path = scope["path"]
107
+ return true if !rel_scope_path.is_a?(String) || rel_scope_path.empty?
107
108
 
108
- sanitized_path = Pathname.new(sanitize_path(path))
109
- rel_scope_path = Pathname.new(scope["path"])
109
+ sanitized_path = sanitize_path(path)
110
110
 
111
- if scope["path"].to_s.include?("*")
111
+ if rel_scope_path.include?("*")
112
112
  glob_scope(sanitized_path, rel_scope_path)
113
113
  else
114
- path_is_subpath?(sanitized_path, strip_collections_dir(scope["path"]))
114
+ path_is_subpath?(sanitized_path, strip_collections_dir(rel_scope_path))
115
115
  end
116
116
  end
117
117
 
@@ -134,11 +134,7 @@ module Jekyll
134
134
  end
135
135
 
136
136
  def path_is_subpath?(path, parent_path)
137
- path.ascend do |ascended_path|
138
- return true if ascended_path.to_s == parent_path.to_s
139
- end
140
-
141
- false
137
+ path.start_with?(parent_path)
142
138
  end
143
139
 
144
140
  def strip_collections_dir(path)
@@ -179,7 +175,7 @@ module Jekyll
179
175
  # new_scope - the new scope hash
180
176
  #
181
177
  # Returns true if the new scope has precedence over the older
182
- # rubocop: disable PredicateName
178
+ # rubocop: disable Naming/PredicateName
183
179
  def has_precedence?(old_scope, new_scope)
184
180
  return true if old_scope.nil?
185
181
 
@@ -194,7 +190,7 @@ module Jekyll
194
190
  !old_scope.key? "type"
195
191
  end
196
192
  end
197
- # rubocop: enable PredicateName
193
+ # rubocop: enable Naming/PredicateName
198
194
 
199
195
  # Collects a list of sets that match the given path and type
200
196
  #
@@ -230,15 +226,14 @@ module Jekyll
230
226
  end.compact
231
227
  end
232
228
 
233
- # Sanitizes the given path by removing a leading and adding a trailing slash
234
-
235
- SANITIZATION_REGEX = %r!\A/|(?<=[^/])\z!.freeze
236
-
229
+ # Sanitizes the given path by removing a leading slash
237
230
  def sanitize_path(path)
238
231
  if path.nil? || path.empty?
239
232
  ""
233
+ elsif path.start_with?("/")
234
+ path.gsub(%r!\A/|(?<=[^/])\z!, "")
240
235
  else
241
- path.gsub(SANITIZATION_REGEX, "")
236
+ path
242
237
  end
243
238
  end
244
239
  end