jekyll 4.0.0.pre.alpha1 → 4.1.1

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