jekyll 3.9.1 → 4.0.0.pre.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +27 -50
  3. data/LICENSE +1 -1
  4. data/README.markdown +46 -17
  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/main.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.rb +5 -0
  11. data/lib/jekyll/cache.rb +183 -0
  12. data/lib/jekyll/cleaner.rb +2 -1
  13. data/lib/jekyll/collection.rb +78 -8
  14. data/lib/jekyll/command.rb +31 -6
  15. data/lib/jekyll/commands/build.rb +11 -20
  16. data/lib/jekyll/commands/clean.rb +2 -0
  17. data/lib/jekyll/commands/doctor.rb +15 -8
  18. data/lib/jekyll/commands/help.rb +1 -1
  19. data/lib/jekyll/commands/new.rb +37 -39
  20. data/lib/jekyll/commands/new_theme.rb +30 -28
  21. data/lib/jekyll/commands/serve.rb +46 -80
  22. data/lib/jekyll/commands/serve/live_reload_reactor.rb +6 -10
  23. data/lib/jekyll/commands/serve/servlet.rb +9 -11
  24. data/lib/jekyll/configuration.rb +26 -26
  25. data/lib/jekyll/converters/identity.rb +18 -0
  26. data/lib/jekyll/converters/markdown.rb +49 -40
  27. data/lib/jekyll/converters/markdown/kramdown_parser.rb +1 -10
  28. data/lib/jekyll/converters/smartypants.rb +34 -14
  29. data/lib/jekyll/convertible.rb +11 -13
  30. data/lib/jekyll/deprecator.rb +1 -3
  31. data/lib/jekyll/document.rb +44 -41
  32. data/lib/jekyll/drops/collection_drop.rb +2 -3
  33. data/lib/jekyll/drops/document_drop.rb +2 -1
  34. data/lib/jekyll/drops/drop.rb +3 -6
  35. data/lib/jekyll/drops/excerpt_drop.rb +4 -0
  36. data/lib/jekyll/drops/site_drop.rb +4 -13
  37. data/lib/jekyll/drops/unified_payload_drop.rb +1 -0
  38. data/lib/jekyll/drops/url_drop.rb +1 -0
  39. data/lib/jekyll/entry_filter.rb +2 -1
  40. data/lib/jekyll/excerpt.rb +45 -34
  41. data/lib/jekyll/external.rb +10 -5
  42. data/lib/jekyll/filters.rb +72 -31
  43. data/lib/jekyll/filters/date_filters.rb +6 -3
  44. data/lib/jekyll/filters/grouping_filters.rb +1 -2
  45. data/lib/jekyll/filters/url_filters.rb +6 -1
  46. data/lib/jekyll/frontmatter_defaults.rb +35 -19
  47. data/lib/jekyll/hooks.rb +2 -3
  48. data/lib/jekyll/liquid_extensions.rb +0 -2
  49. data/lib/jekyll/liquid_renderer.rb +13 -1
  50. data/lib/jekyll/liquid_renderer/file.rb +14 -3
  51. data/lib/jekyll/liquid_renderer/table.rb +67 -65
  52. data/lib/jekyll/log_adapter.rb +5 -1
  53. data/lib/jekyll/page.rb +10 -11
  54. data/lib/jekyll/page_without_a_file.rb +0 -4
  55. data/lib/jekyll/plugin.rb +5 -11
  56. data/lib/jekyll/plugin_manager.rb +2 -0
  57. data/lib/jekyll/reader.rb +38 -8
  58. data/lib/jekyll/readers/data_reader.rb +7 -9
  59. data/lib/jekyll/readers/layout_reader.rb +2 -12
  60. data/lib/jekyll/readers/post_reader.rb +29 -17
  61. data/lib/jekyll/readers/static_file_reader.rb +1 -1
  62. data/lib/jekyll/readers/theme_assets_reader.rb +7 -5
  63. data/lib/jekyll/regenerator.rb +4 -12
  64. data/lib/jekyll/renderer.rb +14 -25
  65. data/lib/jekyll/site.rb +78 -34
  66. data/lib/jekyll/static_file.rb +47 -11
  67. data/lib/jekyll/stevenson.rb +2 -3
  68. data/lib/jekyll/tags/highlight.rb +22 -52
  69. data/lib/jekyll/tags/include.rb +22 -38
  70. data/lib/jekyll/tags/link.rb +11 -7
  71. data/lib/jekyll/tags/post_url.rb +17 -16
  72. data/lib/jekyll/theme.rb +12 -23
  73. data/lib/jekyll/theme_builder.rb +91 -89
  74. data/lib/jekyll/url.rb +3 -2
  75. data/lib/jekyll/utils.rb +5 -4
  76. data/lib/jekyll/utils/ansi.rb +1 -1
  77. data/lib/jekyll/utils/exec.rb +0 -1
  78. data/lib/jekyll/utils/internet.rb +2 -4
  79. data/lib/jekyll/utils/platforms.rb +8 -8
  80. data/lib/jekyll/utils/thread_event.rb +1 -5
  81. data/lib/jekyll/utils/win_tz.rb +1 -1
  82. data/lib/jekyll/version.rb +1 -1
  83. data/lib/site_template/.gitignore +2 -0
  84. data/lib/site_template/404.html +1 -0
  85. data/lib/site_template/_config.yml +17 -5
  86. data/lib/site_template/_posts/0000-00-00-welcome-to-jekyll.markdown.erb +5 -1
  87. data/lib/site_template/{about.md → about.markdown} +0 -0
  88. data/lib/site_template/{index.md → index.markdown} +0 -0
  89. data/lib/theme_template/gitignore.erb +1 -0
  90. data/rubocop/jekyll/assert_equal_literal_actual.rb +149 -0
  91. metadata +85 -51
  92. data/lib/jekyll/converters/markdown/rdiscount_parser.rb +0 -37
  93. data/lib/jekyll/converters/markdown/redcarpet_parser.rb +0 -112
  94. data/lib/jekyll/utils/rouge.rb +0 -22
@@ -17,6 +17,7 @@ module Jekyll
17
17
  end
18
18
 
19
19
  private
20
+
20
21
  def fallback_data
21
22
  @fallback_data ||= {}
22
23
  end
@@ -80,6 +80,7 @@ module Jekyll
80
80
  end
81
81
 
82
82
  private
83
+
83
84
  def fallback_data
84
85
  {}
85
86
  end
@@ -35,6 +35,7 @@ module Jekyll
35
35
  next true if symlink?(e)
36
36
  # Do not reject this entry if it is included.
37
37
  next false if included?(e)
38
+
38
39
  # Reject this entry if it is special, a backup file, or excluded.
39
40
  special?(e) || backup?(e) || excluded?(e)
40
41
  end
@@ -55,7 +56,7 @@ module Jekyll
55
56
  end
56
57
 
57
58
  def excluded?(entry)
58
- glob_include?(site.exclude, relative_to_source(entry)).tap do |excluded|
59
+ glob_include?(site.exclude - site.include, relative_to_source(entry)).tap do |excluded|
59
60
  if excluded
60
61
  Jekyll.logger.debug(
61
62
  "EntryFilter:",
@@ -8,10 +8,11 @@ module Jekyll
8
8
  attr_accessor :content, :ext
9
9
  attr_writer :output
10
10
 
11
- def_delegators :@doc, :site, :name, :ext, :extname,
12
- :collection, :related_posts,
13
- :coffeescript_file?, :yaml_file?,
14
- :url, :next_doc, :previous_doc
11
+ def_delegators :@doc,
12
+ :site, :name, :ext, :extname,
13
+ :collection, :related_posts,
14
+ :coffeescript_file?, :yaml_file?,
15
+ :url, :next_doc, :previous_doc
15
16
 
16
17
  private :coffeescript_file?, :yaml_file?
17
18
 
@@ -48,14 +49,14 @@ module Jekyll
48
49
  #
49
50
  # Returns the relative_path for the doc this excerpt belongs to with #excerpt appended
50
51
  def relative_path
51
- File.join(doc.relative_path, "#excerpt")
52
+ @relative_path ||= File.join(doc.relative_path, "#excerpt")
52
53
  end
53
54
 
54
55
  # Check if excerpt includes a string
55
56
  #
56
57
  # Returns true if the string passed in
57
58
  def include?(something)
58
- (output && output.include?(something)) || content.include?(something)
59
+ (output&.include?(something)) || content.include?(something)
59
60
  end
60
61
 
61
62
  # The UID for this doc (useful in feeds).
@@ -76,7 +77,7 @@ module Jekyll
76
77
 
77
78
  # Returns the shorthand String identifier of this doc.
78
79
  def inspect
79
- "<Excerpt: #{self.id}>"
80
+ "<#{self.class} id=#{id}>"
80
81
  end
81
82
 
82
83
  def output
@@ -88,6 +89,8 @@ module Jekyll
88
89
  end
89
90
 
90
91
  def render_with_liquid?
92
+ return false if data["render_with_liquid"] == false
93
+
91
94
  !(coffeescript_file? || yaml_file? || !Utils.has_liquid_construct?(content))
92
95
  end
93
96
 
@@ -128,36 +131,47 @@ module Jekyll
128
131
  #
129
132
  # Returns excerpt String
130
133
 
131
- LIQUID_TAG_REGEX = %r!{%-?\s*(\w+)\s*.*?-?%}!m
132
- MKDWN_LINK_REF_REGEX = %r!^ {0,3}\[[^\]]+\]:.+$!
134
+ LIQUID_TAG_REGEX = %r!{%-?\s*(\w+)\s*.*?-?%}!m.freeze
135
+ MKDWN_LINK_REF_REGEX = %r!^ {0,3}(?:(\[[^\]]+\])(:.+))$!.freeze
133
136
 
134
137
  def extract_excerpt(doc_content)
135
138
  head, _, tail = doc_content.to_s.partition(doc.excerpt_separator)
139
+ return head if tail.empty?
136
140
 
137
- # append appropriate closing tag(s) (for each Liquid block), to the `head`
138
- # if the partitioning resulted in leaving the closing tag somewhere
139
- # in the `tail` partition.
141
+ head = sanctify_liquid_tags(head) if head.include?("{%")
142
+ definitions = extract_markdown_link_reference_defintions(head, tail)
143
+ return head if definitions.empty?
140
144
 
141
- if head.include?("{%")
142
- modified = false
143
- tag_names = head.scan(LIQUID_TAG_REGEX)
144
- tag_names.flatten!
145
- tag_names.reverse_each do |tag_name|
146
- next unless liquid_block?(tag_name)
147
- next if head =~ endtag_regex_stash(tag_name)
145
+ head << "\n\n" << definitions.join("\n")
146
+ end
148
147
 
149
- modified = true
150
- head << "\n{% end#{tag_name} %}"
151
- end
152
- print_build_warning if modified
153
- end
148
+ private
154
149
 
155
- return head if tail.empty?
150
+ # append appropriate closing tag(s) (for each Liquid block), to the `head` if the
151
+ # partitioning resulted in leaving the closing tag somewhere in the `tail` partition.
152
+ def sanctify_liquid_tags(head)
153
+ modified = false
154
+ tag_names = head.scan(LIQUID_TAG_REGEX)
155
+ tag_names.flatten!
156
+ tag_names.reverse_each do |tag_name|
157
+ next unless liquid_block?(tag_name)
158
+ next if head =~ endtag_regex_stash(tag_name)
159
+
160
+ modified = true
161
+ head << "\n{% end#{tag_name} %}"
162
+ end
156
163
 
157
- head << "\n\n" << tail.scan(MKDWN_LINK_REF_REGEX).join("\n")
164
+ print_build_warning if modified
165
+ head
158
166
  end
159
167
 
160
- private
168
+ def extract_markdown_link_reference_defintions(head, tail)
169
+ [].tap do |definitions|
170
+ tail.scan(MKDWN_LINK_REF_REGEX).each do |segments|
171
+ definitions << segments.join if head.include?(segments[0])
172
+ end
173
+ end
174
+ end
161
175
 
162
176
  def endtag_regex_stash(tag_name)
163
177
  @endtag_regex_stash ||= {}
@@ -171,8 +185,7 @@ module Jekyll
171
185
  Liquid::Template.tags[tag_name].ancestors.include?(Liquid::Block)
172
186
  rescue NoMethodError
173
187
  Jekyll.logger.error "Error:",
174
- "A Liquid tag in the excerpt of #{doc.relative_path} couldn't be " \
175
- "parsed."
188
+ "A Liquid tag in the excerpt of #{doc.relative_path} couldn't be parsed."
176
189
  raise
177
190
  end
178
191
 
@@ -180,11 +193,9 @@ module Jekyll
180
193
  Jekyll.logger.warn "Warning:", "Excerpt modified in #{doc.relative_path}!"
181
194
  Jekyll.logger.warn "", "Found a Liquid block containing the excerpt separator" \
182
195
  " #{doc.excerpt_separator.inspect}. "
183
- Jekyll.logger.warn "", "The block has been modified with the appropriate" \
184
- " closing tag."
185
- Jekyll.logger.warn "", "Feel free to define a custom excerpt or" \
186
- " excerpt_separator in the document's front matter" \
187
- " if the generated excerpt is unsatisfactory."
196
+ Jekyll.logger.warn "", "The block has been modified with the appropriate closing tag."
197
+ Jekyll.logger.warn "", "Feel free to define a custom excerpt or excerpt_separator in the"
198
+ Jekyll.logger.warn "", "document's Front Matter if the generated excerpt is unsatisfactory."
188
199
  end
189
200
  end
190
201
  end
@@ -9,6 +9,7 @@ module Jekyll
9
9
  #
10
10
  def blessed_gems
11
11
  %w(
12
+ jekyll-compose
12
13
  jekyll-docs
13
14
  jekyll-import
14
15
  )
@@ -41,6 +42,7 @@ module Jekyll
41
42
  # RubyGems.
42
43
  def version_constraint(gem_name)
43
44
  return "= #{Jekyll::VERSION}" if gem_name.to_s.eql?("jekyll-docs")
45
+
44
46
  "> 0"
45
47
  end
46
48
 
@@ -57,13 +59,16 @@ module Jekyll
57
59
  Jekyll.logger.debug "Requiring:", name.to_s
58
60
  require name
59
61
  rescue LoadError => e
60
- Jekyll.logger.error "Dependency Error:", <<-MSG
61
- Yikes! It looks like you don't have #{name} or one of its dependencies installed.
62
- In order to use Jekyll as currently configured, you'll need to install this gem.
62
+ Jekyll.logger.error "Dependency Error:", <<~MSG
63
+ Yikes! It looks like you don't have #{name} or one of its dependencies installed.
64
+ In order to use Jekyll as currently configured, you'll need to install this gem.
65
+
66
+ If you've run Jekyll with `bundle exec`, ensure that you have included the #{name}
67
+ gem in your Gemfile as well.
63
68
 
64
- The full error message from Ruby is: '#{e.message}'
69
+ The full error message from Ruby is: '#{e.message}'
65
70
 
66
- If you run into trouble, you can find helpful resources at https://jekyllrb.com/help/!
71
+ If you run into trouble, you can find helpful resources at https://jekyllrb.com/help/!
67
72
  MSG
68
73
  raise Jekyll::Errors::MissingDependencyException, name
69
74
  end
@@ -169,6 +169,7 @@ module Jekyll
169
169
  def where(input, property, value)
170
170
  return input if property.nil? || value.nil?
171
171
  return input unless input.respond_to?(:select)
172
+
172
173
  input = input.values if input.is_a?(Hash)
173
174
  input_id = input.hash
174
175
 
@@ -195,6 +196,7 @@ module Jekyll
195
196
  # Returns the filtered array of objects
196
197
  def where_exp(input, variable, expression)
197
198
  return input unless input.respond_to?(:select)
199
+
198
200
  input = input.values if input.is_a?(Hash) # FIXME
199
201
 
200
202
  condition = parse_condition(expression)
@@ -214,6 +216,7 @@ module Jekyll
214
216
  def to_integer(input)
215
217
  return 1 if input == true
216
218
  return 0 if input == false
219
+
217
220
  input.to_i
218
221
  end
219
222
 
@@ -225,9 +228,8 @@ module Jekyll
225
228
  #
226
229
  # Returns the filtered array of objects
227
230
  def sort(input, property = nil, nils = "first")
228
- if input.nil?
229
- raise ArgumentError, "Cannot sort a null object."
230
- end
231
+ raise ArgumentError, "Cannot sort a null object." if input.nil?
232
+
231
233
  if property.nil?
232
234
  input.sort
233
235
  else
@@ -246,6 +248,7 @@ module Jekyll
246
248
 
247
249
  def pop(array, num = 1)
248
250
  return array unless array.is_a?(Array)
251
+
249
252
  num = Liquid::Utils.to_integer(num)
250
253
  new_ary = array.dup
251
254
  new_ary.pop(num)
@@ -254,6 +257,7 @@ module Jekyll
254
257
 
255
258
  def push(array, input)
256
259
  return array unless array.is_a?(Array)
260
+
257
261
  new_ary = array.dup
258
262
  new_ary.push(input)
259
263
  new_ary
@@ -261,6 +265,7 @@ module Jekyll
261
265
 
262
266
  def shift(array, num = 1)
263
267
  return array unless array.is_a?(Array)
268
+
264
269
  num = Liquid::Utils.to_integer(num)
265
270
  new_ary = array.dup
266
271
  new_ary.shift(num)
@@ -269,6 +274,7 @@ module Jekyll
269
274
 
270
275
  def unshift(array, input)
271
276
  return array unless array.is_a?(Array)
277
+
272
278
  new_ary = array.dup
273
279
  new_ary.unshift(input)
274
280
  new_ary
@@ -276,6 +282,7 @@ module Jekyll
276
282
 
277
283
  def sample(input, num = 1)
278
284
  return input unless input.respond_to?(:sample)
285
+
279
286
  num = Liquid::Utils.to_integer(num) rescue 1
280
287
  if num == 1
281
288
  input.sample
@@ -301,35 +308,45 @@ module Jekyll
301
308
  # We also utilize the Schwartzian transform to make this more efficient.
302
309
  def sort_input(input, property, order)
303
310
  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
311
+ .sort! do |a_info, b_info|
312
+ a_property = a_info.first
313
+ b_property = b_info.first
307
314
 
308
- if !apple_property.nil? && orange_property.nil?
315
+ if !a_property.nil? && b_property.nil?
309
316
  - order
310
- elsif apple_property.nil? && !orange_property.nil?
317
+ elsif a_property.nil? && !b_property.nil?
311
318
  + order
312
319
  else
313
- apple_property <=> orange_property
320
+ a_property <=> b_property || a_property.to_s <=> b_property.to_s
314
321
  end
315
322
  end
316
323
  .map!(&:last)
317
324
  end
318
325
 
319
- private
320
326
  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]
327
+ @item_property_cache ||= {}
328
+ @item_property_cache[property] ||= {}
329
+ @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])
324
338
  end
325
- elsif item.respond_to?(:data)
326
- item.data[property.to_s]
327
- else
328
- item[property.to_s]
329
339
  end
330
340
  end
331
341
 
332
- private
342
+ # return numeric values as numbers for proper sorting
343
+ 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
346
+
347
+ property
348
+ end
349
+
333
350
  def as_liquid(item)
334
351
  case item
335
352
  when Hash
@@ -352,25 +369,49 @@ module Jekyll
352
369
  end
353
370
  end
354
371
 
372
+ # ----------- The following set of code was *adapted* from Liquid::If
373
+ # ----------- ref: https://git.io/vp6K6
374
+
355
375
  # Parse a string to a Liquid Condition
356
- private
357
376
  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)
377
+ parser = Liquid::Parser.new(exp)
378
+ condition = parse_binary_comparison(parser)
370
379
 
380
+ parser.consume(:end_of_string)
371
381
  condition
372
382
  end
373
383
 
384
+ # Generate a Liquid::Condition object from a Liquid::Parser object additionally processing
385
+ # the parsed expression based on whether the expression consists of binary operations with
386
+ # Liquid operators `and` or `or`
387
+ #
388
+ # - parser: an instance of Liquid::Parser
389
+ #
390
+ # Returns an instance of Liquid::Condition
391
+ 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
395
+ end
396
+ end
397
+
398
+ # Generates a Liquid::Condition object from a Liquid::Parser object based on whether the parsed
399
+ # expression involves a "comparison" operator (e.g. <, ==, >, !=, etc)
400
+ #
401
+ # - parser: an instance of Liquid::Parser
402
+ #
403
+ # Returns an instance of Liquid::Condition
404
+ def parse_comparison(parser)
405
+ left_operand = Liquid::Expression.parse(parser.expression)
406
+ operator = parser.consume?(:comparison)
407
+
408
+ # No comparison-operator detected. Initialize a Liquid::Condition using only left operand
409
+ return Liquid::Condition.new(left_operand) unless operator
410
+
411
+ # Parse what remained after extracting the left operand and the `:comparison` operator
412
+ # and initialize a Liquid::Condition object using the operands and the comparison-operator
413
+ Liquid::Condition.new(left_operand, operator, Liquid::Expression.parse(parser.expression))
414
+ end
374
415
  end
375
416
  end
376
417
 
@@ -45,6 +45,7 @@ module Jekyll
45
45
  # Returns the formatted String.
46
46
  def date_to_xmlschema(date)
47
47
  return date if date.to_s.empty?
48
+
48
49
  time(date).xmlschema
49
50
  end
50
51
 
@@ -60,10 +61,12 @@ module Jekyll
60
61
  # Returns the formatted String.
61
62
  def date_to_rfc822(date)
62
63
  return date if date.to_s.empty?
64
+
63
65
  time(date).rfc822
64
66
  end
65
67
 
66
68
  private
69
+
67
70
  # month_type: Notations that evaluate to 'Month' via `Time#strftime` ("%b", "%B")
68
71
  # type: nil (default) or "ordinal"
69
72
  # style: nil (default) or "US"
@@ -71,17 +74,18 @@ module Jekyll
71
74
  # Returns a stringified date or the empty input.
72
75
  def stringify_date(date, month_type, type = nil, style = nil)
73
76
  return date if date.to_s.empty?
77
+
74
78
  time = time(date)
75
79
  if type == "ordinal"
76
80
  day = time.day
77
81
  ordinal_day = "#{day}#{ordinal(day)}"
78
82
  return time.strftime("#{month_type} #{ordinal_day}, %Y") if style == "US"
83
+
79
84
  return time.strftime("#{ordinal_day} #{month_type} %Y")
80
85
  end
81
86
  time.strftime("%d #{month_type} %Y")
82
87
  end
83
88
 
84
- private
85
89
  def ordinal(number)
86
90
  return "th" if (11..13).cover?(number)
87
91
 
@@ -93,12 +97,11 @@ module Jekyll
93
97
  end
94
98
  end
95
99
 
96
- private
97
100
  def time(input)
98
101
  date = Liquid::Utils.to_date(input)
99
102
  unless date.respond_to?(:to_time)
100
103
  raise Errors::InvalidDateError,
101
- "Invalid Date: '#{input.inspect}' is not a valid datetime."
104
+ "Invalid Date: '#{input.inspect}' is not a valid datetime."
102
105
  end
103
106
  date.to_time.dup.localtime
104
107
  end