jekyll 4.0.0.pre.alpha1 → 4.1.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +58 -17
  3. data/lib/jekyll.rb +5 -1
  4. data/lib/jekyll/cache.rb +71 -64
  5. data/lib/jekyll/cleaner.rb +5 -5
  6. data/lib/jekyll/collection.rb +6 -4
  7. data/lib/jekyll/command.rb +4 -2
  8. data/lib/jekyll/commands/new.rb +4 -4
  9. data/lib/jekyll/commands/serve.rb +9 -1
  10. data/lib/jekyll/commands/serve/servlet.rb +13 -14
  11. data/lib/jekyll/commands/serve/websockets.rb +1 -1
  12. data/lib/jekyll/configuration.rb +40 -129
  13. data/lib/jekyll/converters/identity.rb +2 -2
  14. data/lib/jekyll/converters/markdown/kramdown_parser.rb +70 -9
  15. data/lib/jekyll/convertible.rb +21 -20
  16. data/lib/jekyll/document.rb +56 -31
  17. data/lib/jekyll/drops/document_drop.rb +12 -0
  18. data/lib/jekyll/drops/drop.rb +14 -8
  19. data/lib/jekyll/drops/site_drop.rb +11 -1
  20. data/lib/jekyll/drops/url_drop.rb +52 -1
  21. data/lib/jekyll/entry_filter.rb +38 -44
  22. data/lib/jekyll/excerpt.rb +3 -3
  23. data/lib/jekyll/filters.rb +140 -22
  24. data/lib/jekyll/filters/url_filters.rb +41 -14
  25. data/lib/jekyll/frontmatter_defaults.rb +15 -20
  26. data/lib/jekyll/hooks.rb +2 -5
  27. data/lib/jekyll/inclusion.rb +32 -0
  28. data/lib/jekyll/liquid_renderer.rb +18 -15
  29. data/lib/jekyll/liquid_renderer/file.rb +10 -0
  30. data/lib/jekyll/liquid_renderer/table.rb +18 -61
  31. data/lib/jekyll/mime.types +53 -11
  32. data/lib/jekyll/page.rb +26 -2
  33. data/lib/jekyll/page_excerpt.rb +25 -0
  34. data/lib/jekyll/path_manager.rb +31 -0
  35. data/lib/jekyll/profiler.rb +58 -0
  36. data/lib/jekyll/reader.rb +4 -1
  37. data/lib/jekyll/readers/collection_reader.rb +1 -0
  38. data/lib/jekyll/readers/data_reader.rb +1 -0
  39. data/lib/jekyll/readers/layout_reader.rb +1 -0
  40. data/lib/jekyll/readers/page_reader.rb +5 -5
  41. data/lib/jekyll/readers/post_reader.rb +2 -1
  42. data/lib/jekyll/readers/static_file_reader.rb +3 -3
  43. data/lib/jekyll/readers/theme_assets_reader.rb +1 -0
  44. data/lib/jekyll/renderer.rb +9 -15
  45. data/lib/jekyll/site.rb +21 -12
  46. data/lib/jekyll/static_file.rb +15 -10
  47. data/lib/jekyll/tags/highlight.rb +2 -4
  48. data/lib/jekyll/tags/include.rb +67 -11
  49. data/lib/jekyll/tags/post_url.rb +8 -5
  50. data/lib/jekyll/theme.rb +19 -10
  51. data/lib/jekyll/url.rb +7 -3
  52. data/lib/jekyll/utils.rb +14 -18
  53. data/lib/jekyll/utils/platforms.rb +1 -1
  54. data/lib/jekyll/utils/win_tz.rb +1 -1
  55. data/lib/jekyll/version.rb +1 -1
  56. data/lib/theme_template/theme.gemspec.erb +1 -4
  57. metadata +33 -36
@@ -30,7 +30,6 @@ module Jekyll
30
30
  # Returns nothing
31
31
  def initialize(obj)
32
32
  @obj = obj
33
- @mutations = {} # only if mutable: true
34
33
  end
35
34
 
36
35
  # Access a method in the Drop or a field in the underlying hash data.
@@ -42,8 +41,8 @@ module Jekyll
42
41
  #
43
42
  # Returns the value for the given key, or nil if none exists
44
43
  def [](key)
45
- if self.class.mutable? && @mutations.key?(key)
46
- @mutations[key]
44
+ if self.class.mutable? && mutations.key?(key)
45
+ mutations[key]
47
46
  elsif self.class.invokable? key
48
47
  public_send key
49
48
  else
@@ -66,11 +65,12 @@ module Jekyll
66
65
  # and the key matches a method in which case it raises a
67
66
  # DropMutationException.
68
67
  def []=(key, val)
69
- if respond_to?("#{key}=")
70
- public_send("#{key}=", val)
68
+ setter = "#{key}="
69
+ if respond_to?(setter)
70
+ public_send(setter, val)
71
71
  elsif respond_to?(key.to_s)
72
72
  if self.class.mutable?
73
- @mutations[key] = val
73
+ mutations[key] = val
74
74
  else
75
75
  raise Errors::DropMutationException, "Key #{key} cannot be set in the drop."
76
76
  end
@@ -100,7 +100,7 @@ module Jekyll
100
100
  # Returns true if the given key is present
101
101
  def key?(key)
102
102
  return false if key.nil?
103
- return true if self.class.mutable? && @mutations.key?(key)
103
+ return true if self.class.mutable? && mutations.key?(key)
104
104
 
105
105
  respond_to?(key) || fallback_data.key?(key)
106
106
  end
@@ -113,7 +113,7 @@ module Jekyll
113
113
  # Returns an Array of unique keys for content for the Drop.
114
114
  def keys
115
115
  (content_methods |
116
- @mutations.keys |
116
+ mutations.keys |
117
117
  fallback_data.keys).flatten
118
118
  end
119
119
 
@@ -204,6 +204,12 @@ module Jekyll
204
204
  return yield(key) unless block.nil?
205
205
  return default unless default.nil?
206
206
  end
207
+
208
+ private
209
+
210
+ def mutations
211
+ @mutations ||= {}
212
+ end
207
213
  end
208
214
  end
209
215
  end
@@ -8,7 +8,7 @@ module Jekyll
8
8
  mutable false
9
9
 
10
10
  def_delegator :@obj, :site_data, :data
11
- def_delegators :@obj, :time, :pages, :static_files, :documents, :tags, :categories
11
+ def_delegators :@obj, :time, :pages, :static_files, :tags, :categories
12
12
 
13
13
  private def_delegator :@obj, :config, :fallback_data
14
14
 
@@ -38,6 +38,16 @@ module Jekyll
38
38
  @site_collections ||= @obj.collections.values.sort_by(&:label).map(&:to_liquid)
39
39
  end
40
40
 
41
+ # `Site#documents` cannot be memoized so that `Site#docs_to_write` can access the
42
+ # latest state of the attribute.
43
+ #
44
+ # Since this method will be called after `Site#pre_render` hook, the `Site#documents`
45
+ # array shouldn't thereafter change and can therefore be safely memoized to prevent
46
+ # additional computation of `Site#documents`.
47
+ def documents
48
+ @documents ||= @obj.documents
49
+ end
50
+
41
51
  # `{{ site.related_posts }}` is how posts can get posts related to
42
52
  # them, either through LSI if it's enabled, or through the most
43
53
  # recent posts.
@@ -35,46 +35,97 @@ 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
+
46
+ # CCYY
38
47
  def year
39
48
  @obj.date.strftime("%Y")
40
49
  end
41
50
 
51
+ # MM: 01..12
42
52
  def month
43
53
  @obj.date.strftime("%m")
44
54
  end
45
55
 
56
+ # DD: 01..31
46
57
  def day
47
58
  @obj.date.strftime("%d")
48
59
  end
49
60
 
61
+ # hh: 00..23
50
62
  def hour
51
63
  @obj.date.strftime("%H")
52
64
  end
53
65
 
66
+ # mm: 00..59
54
67
  def minute
55
68
  @obj.date.strftime("%M")
56
69
  end
57
70
 
71
+ # ss: 00..59
58
72
  def second
59
73
  @obj.date.strftime("%S")
60
74
  end
61
75
 
76
+ # D: 1..31
62
77
  def i_day
63
78
  @obj.date.strftime("%-d")
64
79
  end
65
80
 
81
+ # M: 1..12
66
82
  def i_month
67
83
  @obj.date.strftime("%-m")
68
84
  end
69
85
 
86
+ # MMM: Jan..Dec
70
87
  def short_month
71
88
  @obj.date.strftime("%b")
72
89
  end
73
90
 
91
+ # MMMM: January..December
92
+ def long_month
93
+ @obj.date.strftime("%B")
94
+ end
95
+
96
+ # YY: 00..99
74
97
  def short_year
75
98
  @obj.date.strftime("%y")
76
99
  end
77
100
 
101
+ # CCYYw, ISO week year
102
+ # may differ from CCYY for the first days of January and last days of December
103
+ def w_year
104
+ @obj.date.strftime("%G")
105
+ end
106
+
107
+ # WW: 01..53
108
+ # %W and %U do not comply with ISO 8601-1
109
+ def week
110
+ @obj.date.strftime("%V")
111
+ end
112
+
113
+ # d: 1..7 (Monday..Sunday)
114
+ def w_day
115
+ @obj.date.strftime("%u")
116
+ end
117
+
118
+ # dd: Mon..Sun
119
+ def short_day
120
+ @obj.date.strftime("%a")
121
+ end
122
+
123
+ # ddd: Monday..Sunday
124
+ def long_day
125
+ @obj.date.strftime("%A")
126
+ end
127
+
128
+ # DDD: 001..366
78
129
  def y_day
79
130
  @obj.date.strftime("%j")
80
131
  end
@@ -82,7 +133,7 @@ module Jekyll
82
133
  private
83
134
 
84
135
  def fallback_data
85
- {}
136
+ @fallback_data ||= {}
86
137
  end
87
138
  end
88
139
  end
@@ -3,9 +3,8 @@
3
3
  module Jekyll
4
4
  class EntryFilter
5
5
  attr_reader :site
6
- SPECIAL_LEADING_CHARACTERS = [
7
- ".", "_", "#", "~",
8
- ].freeze
6
+
7
+ SPECIAL_LEADING_CHAR_REGEX = %r!\A#{Regexp.union([".", "_", "#", "~"])}!o.freeze
9
8
 
10
9
  def initialize(site, base_directory = nil)
11
10
  @site = site
@@ -31,13 +30,24 @@ module Jekyll
31
30
 
32
31
  def filter(entries)
33
32
  entries.reject do |e|
34
- # Reject this entry if it is a symlink.
33
+ # Reject this entry if it is just a "dot" representation.
34
+ # e.g.: '.', '..', '_movies/.', 'music/..', etc
35
+ next true if e.end_with?(".")
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.
35
44
  next true if symlink?(e)
36
- # Do not reject this entry if it is included.
37
- next false if included?(e)
38
45
 
39
- # Reject this entry if it is special, a backup file, or excluded.
40
- 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)
41
51
  end
42
52
  end
43
53
 
@@ -47,12 +57,12 @@ module Jekyll
47
57
  end
48
58
 
49
59
  def special?(entry)
50
- SPECIAL_LEADING_CHARACTERS.include?(entry[0..0]) ||
51
- SPECIAL_LEADING_CHARACTERS.include?(File.basename(entry)[0..0])
60
+ SPECIAL_LEADING_CHAR_REGEX.match?(entry) ||
61
+ SPECIAL_LEADING_CHAR_REGEX.match?(File.basename(entry))
52
62
  end
53
63
 
54
64
  def backup?(entry)
55
- entry[-1..-1] == "~"
65
+ entry.end_with?("~")
56
66
  end
57
67
 
58
68
  def excluded?(entry)
@@ -86,40 +96,24 @@ module Jekyll
86
96
  )
87
97
  end
88
98
 
89
- # --
90
- # Check if an entry matches a specific pattern and return true,false.
91
- # Returns true if path matches against any glob pattern.
92
- # --
93
- def glob_include?(enum, entry)
94
- entry_path = Pathutil.new(site.in_source_dir).join(entry)
95
- enum.any? do |exp|
96
- # Users who send a Regexp knows what they want to
97
- # exclude, so let them send a Regexp to exclude files,
98
- # we will not bother caring if it works or not, it's
99
- # on them at this point.
100
-
101
- if exp.is_a?(Regexp)
102
- entry_path =~ exp
103
-
99
+ # Check if an entry matches a specific pattern.
100
+ # Returns true if path matches against any glob pattern, else false.
101
+ def glob_include?(enumerator, entry)
102
+ entry_with_source = PathManager.join(site.source, entry)
103
+ entry_is_directory = File.directory?(entry_with_source)
104
+
105
+ enumerator.any? do |pattern|
106
+ case pattern
107
+ when String
108
+ pattern_with_source = PathManager.join(site.source, pattern)
109
+
110
+ File.fnmatch?(pattern_with_source, entry_with_source) ||
111
+ entry_with_source.start_with?(pattern_with_source) ||
112
+ (pattern_with_source == "#{entry_with_source}/" if entry_is_directory)
113
+ when Regexp
114
+ pattern.match?(entry_with_source)
104
115
  else
105
- item = Pathutil.new(site.in_source_dir).join(exp)
106
-
107
- # If it's a directory they want to exclude, AKA
108
- # ends with a "/" then we will go on to check and
109
- # see if the entry falls within that path and
110
- # exclude it if that's the case.
111
-
112
- if entry.end_with?("/")
113
- entry_path.in_path?(
114
- item
115
- )
116
-
117
- else
118
- File.fnmatch?(item, entry_path) ||
119
- entry_path.to_path.start_with?(
120
- item
121
- )
122
- end
116
+ false
123
117
  end
124
118
  end
125
119
  end
@@ -10,7 +10,7 @@ module Jekyll
10
10
 
11
11
  def_delegators :@doc,
12
12
  :site, :name, :ext, :extname,
13
- :collection, :related_posts,
13
+ :collection, :related_posts, :type,
14
14
  :coffeescript_file?, :yaml_file?,
15
15
  :url, :next_doc, :previous_doc
16
16
 
@@ -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).
@@ -155,7 +155,7 @@ module Jekyll
155
155
  tag_names.flatten!
156
156
  tag_names.reverse_each do |tag_name|
157
157
  next unless liquid_block?(tag_name)
158
- next if head =~ endtag_regex_stash(tag_name)
158
+ next if endtag_regex_stash(tag_name).match?(head)
159
159
 
160
160
  modified = true
161
161
  head << "\n{% end#{tag_name} %}"
@@ -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,13 +173,15 @@ 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)
172
186
 
173
187
  input = input.values if input.is_a?(Hash)
@@ -182,8 +196,8 @@ module Jekyll
182
196
  # stash or retrive results to return
183
197
  @where_filter_cache[input_id][property][value] ||= begin
184
198
  input.select do |object|
185
- Array(item_property(object, property)).map!(&:to_s).include?(value.to_s)
186
- end || []
199
+ compare_property_vs_target(item_property(object, property), value)
200
+ end.to_a
187
201
  end
188
202
  end
189
203
 
@@ -208,6 +222,66 @@ module Jekyll
208
222
  end || []
209
223
  end
210
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
+
211
285
  # Convert the input into integer
212
286
  #
213
287
  # input - the object string
@@ -323,26 +397,66 @@ module Jekyll
323
397
  .map!(&:last)
324
398
  end
325
399
 
400
+ # `where` filter helper
401
+ #
402
+ # rubocop:disable Metrics/PerceivedComplexity
403
+ def compare_property_vs_target(property, target)
404
+ case target
405
+ when NilClass
406
+ return true if property.nil?
407
+ when Liquid::Expression::MethodLiteral # `empty` or `blank`
408
+ target = target.to_s
409
+ return true if property == target || Array(property).join == target
410
+ else
411
+ target = target.to_s
412
+ if property.is_a? String
413
+ return true if property == target
414
+ else
415
+ Array(property).each do |prop|
416
+ return true if prop.to_s == target
417
+ end
418
+ end
419
+ end
420
+
421
+ false
422
+ end
423
+
424
+ # rubocop:enable Metrics/PerceivedComplexity
425
+
326
426
  def item_property(item, property)
327
427
  @item_property_cache ||= {}
328
428
  @item_property_cache[property] ||= {}
329
429
  @item_property_cache[property][item] ||= begin
330
- if item.respond_to?(:to_liquid)
331
- property.to_s.split(".").reduce(item.to_liquid) do |subvalue, attribute|
332
- parse_sort_input(subvalue[attribute])
333
- end
334
- elsif item.respond_to?(:data)
335
- parse_sort_input(item.data[property.to_s])
336
- else
337
- parse_sort_input(item[property.to_s])
338
- end
430
+ property = property.to_s
431
+ property = if item.respond_to?(:to_liquid)
432
+ read_liquid_attribute(item.to_liquid, property)
433
+ elsif item.respond_to?(:data)
434
+ item.data[property]
435
+ else
436
+ item[property]
437
+ end
438
+
439
+ parse_sort_input(property)
440
+ end
441
+ end
442
+
443
+ def read_liquid_attribute(liquid_data, property)
444
+ return liquid_data[property] unless property.include?(".")
445
+
446
+ property.split(".").reduce(liquid_data) do |data, key|
447
+ data.respond_to?(:[]) && data[key]
339
448
  end
340
449
  end
341
450
 
451
+ FLOAT_LIKE = %r!\A\s*-?(?:\d+\.?\d*|\.\d+)\s*\Z!.freeze
452
+ INTEGER_LIKE = %r!\A\s*-?\d+\s*\Z!.freeze
453
+ private_constant :FLOAT_LIKE, :INTEGER_LIKE
454
+
342
455
  # return numeric values as numbers for proper sorting
343
456
  def parse_sort_input(property)
344
- number_like = %r!\A\s*-?(?:\d+\.?\d*|\.\d+)\s*\Z!
345
- return property.to_f if property =~ number_like
457
+ stringified = property.to_s
458
+ return property.to_i if INTEGER_LIKE.match?(stringified)
459
+ return property.to_f if FLOAT_LIKE.match?(stringified)
346
460
 
347
461
  property
348
462
  end
@@ -389,10 +503,14 @@ module Jekyll
389
503
  #
390
504
  # Returns an instance of Liquid::Condition
391
505
  def parse_binary_comparison(parser)
392
- parse_comparison(parser).tap do |condition|
393
- binary_operator = parser.id?("and") || parser.id?("or")
394
- condition.send(binary_operator, parse_comparison(parser)) if binary_operator
506
+ condition = parse_comparison(parser)
507
+ first_condition = condition
508
+ while (binary_operator = parser.id?("and") || parser.id?("or"))
509
+ child_condition = parse_comparison(parser)
510
+ condition.send(binary_operator, child_condition)
511
+ condition = child_condition
395
512
  end
513
+ first_condition
396
514
  end
397
515
 
398
516
  # Generates a Liquid::Condition object from a Liquid::Parser object based on whether the parsed