jekyll 4.2.1 → 4.3.2

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +474 -350
  3. data/LICENSE +21 -21
  4. data/README.markdown +83 -86
  5. data/exe/jekyll +57 -57
  6. data/lib/blank_template/_config.yml +3 -3
  7. data/lib/blank_template/_layouts/default.html +12 -12
  8. data/lib/blank_template/_sass/{main.scss → base.scss} +9 -9
  9. data/lib/blank_template/assets/css/main.scss +4 -4
  10. data/lib/blank_template/index.md +8 -8
  11. data/lib/jekyll/cache.rb +186 -190
  12. data/lib/jekyll/cleaner.rb +111 -111
  13. data/lib/jekyll/collection.rb +310 -309
  14. data/lib/jekyll/command.rb +105 -105
  15. data/lib/jekyll/commands/build.rb +82 -93
  16. data/lib/jekyll/commands/clean.rb +44 -45
  17. data/lib/jekyll/commands/doctor.rb +177 -177
  18. data/lib/jekyll/commands/help.rb +34 -34
  19. data/lib/jekyll/commands/new.rb +168 -169
  20. data/lib/jekyll/commands/new_theme.rb +39 -40
  21. data/lib/jekyll/commands/serve/live_reload_reactor.rb +119 -122
  22. data/lib/jekyll/commands/serve/livereload_assets/livereload.js +1183 -1183
  23. data/lib/jekyll/commands/serve/mime_types_charset.json +71 -0
  24. data/lib/jekyll/commands/serve/servlet.rb +206 -202
  25. data/lib/jekyll/commands/serve/websockets.rb +81 -81
  26. data/lib/jekyll/commands/serve.rb +367 -362
  27. data/lib/jekyll/configuration.rb +313 -313
  28. data/lib/jekyll/converter.rb +54 -54
  29. data/lib/jekyll/converters/identity.rb +41 -41
  30. data/lib/jekyll/converters/markdown/kramdown_parser.rb +197 -199
  31. data/lib/jekyll/converters/markdown.rb +113 -113
  32. data/lib/jekyll/converters/smartypants.rb +70 -70
  33. data/lib/jekyll/convertible.rb +257 -257
  34. data/lib/jekyll/deprecator.rb +50 -50
  35. data/lib/jekyll/document.rb +543 -544
  36. data/lib/jekyll/drops/collection_drop.rb +20 -20
  37. data/lib/jekyll/drops/document_drop.rb +74 -70
  38. data/lib/jekyll/drops/drop.rb +293 -293
  39. data/lib/jekyll/drops/excerpt_drop.rb +23 -19
  40. data/lib/jekyll/drops/jekyll_drop.rb +32 -32
  41. data/lib/jekyll/drops/site_drop.rb +66 -66
  42. data/lib/jekyll/drops/static_file_drop.rb +14 -14
  43. data/lib/jekyll/drops/theme_drop.rb +36 -0
  44. data/lib/jekyll/drops/unified_payload_drop.rb +30 -26
  45. data/lib/jekyll/drops/url_drop.rb +140 -140
  46. data/lib/jekyll/entry_filter.rb +117 -121
  47. data/lib/jekyll/errors.rb +20 -20
  48. data/lib/jekyll/excerpt.rb +200 -201
  49. data/lib/jekyll/external.rb +75 -79
  50. data/lib/jekyll/filters/date_filters.rb +110 -110
  51. data/lib/jekyll/filters/grouping_filters.rb +64 -64
  52. data/lib/jekyll/filters/url_filters.rb +98 -98
  53. data/lib/jekyll/filters.rb +532 -535
  54. data/lib/jekyll/frontmatter_defaults.rb +238 -240
  55. data/lib/jekyll/generator.rb +5 -5
  56. data/lib/jekyll/hooks.rb +107 -107
  57. data/lib/jekyll/inclusion.rb +32 -32
  58. data/lib/jekyll/layout.rb +55 -67
  59. data/lib/jekyll/liquid_extensions.rb +22 -22
  60. data/lib/jekyll/liquid_renderer/file.rb +77 -77
  61. data/lib/jekyll/liquid_renderer/table.rb +45 -55
  62. data/lib/jekyll/liquid_renderer.rb +80 -80
  63. data/lib/jekyll/log_adapter.rb +151 -151
  64. data/lib/jekyll/mime.types +939 -866
  65. data/lib/jekyll/page.rb +215 -217
  66. data/lib/jekyll/page_excerpt.rb +25 -25
  67. data/lib/jekyll/page_without_a_file.rb +14 -14
  68. data/lib/jekyll/path_manager.rb +74 -74
  69. data/lib/jekyll/plugin.rb +92 -92
  70. data/lib/jekyll/plugin_manager.rb +123 -115
  71. data/lib/jekyll/profiler.rb +51 -58
  72. data/lib/jekyll/publisher.rb +23 -23
  73. data/lib/jekyll/reader.rb +209 -192
  74. data/lib/jekyll/readers/collection_reader.rb +23 -23
  75. data/lib/jekyll/readers/data_reader.rb +113 -79
  76. data/lib/jekyll/readers/layout_reader.rb +62 -62
  77. data/lib/jekyll/readers/page_reader.rb +25 -25
  78. data/lib/jekyll/readers/post_reader.rb +85 -85
  79. data/lib/jekyll/readers/static_file_reader.rb +25 -25
  80. data/lib/jekyll/readers/theme_assets_reader.rb +52 -52
  81. data/lib/jekyll/regenerator.rb +195 -195
  82. data/lib/jekyll/related_posts.rb +52 -52
  83. data/lib/jekyll/renderer.rb +263 -265
  84. data/lib/jekyll/site.rb +576 -551
  85. data/lib/jekyll/static_file.rb +205 -208
  86. data/lib/jekyll/stevenson.rb +60 -60
  87. data/lib/jekyll/tags/highlight.rb +114 -110
  88. data/lib/jekyll/tags/include.rb +275 -275
  89. data/lib/jekyll/tags/link.rb +42 -42
  90. data/lib/jekyll/tags/post_url.rb +106 -106
  91. data/lib/jekyll/theme.rb +90 -86
  92. data/lib/jekyll/theme_builder.rb +121 -121
  93. data/lib/jekyll/url.rb +167 -167
  94. data/lib/jekyll/utils/ansi.rb +57 -57
  95. data/lib/jekyll/utils/exec.rb +26 -26
  96. data/lib/jekyll/utils/internet.rb +37 -37
  97. data/lib/jekyll/utils/platforms.rb +67 -67
  98. data/lib/jekyll/utils/thread_event.rb +31 -31
  99. data/lib/jekyll/utils/win_tz.rb +46 -75
  100. data/lib/jekyll/utils.rb +371 -367
  101. data/lib/jekyll/version.rb +5 -5
  102. data/lib/jekyll.rb +195 -195
  103. data/lib/site_template/.gitignore +5 -5
  104. data/lib/site_template/404.html +25 -25
  105. data/lib/site_template/_config.yml +55 -55
  106. data/lib/site_template/_posts/0000-00-00-welcome-to-jekyll.markdown.erb +29 -29
  107. data/lib/site_template/about.markdown +18 -18
  108. data/lib/site_template/index.markdown +6 -6
  109. data/lib/theme_template/CODE_OF_CONDUCT.md.erb +74 -74
  110. data/lib/theme_template/Gemfile +4 -4
  111. data/lib/theme_template/LICENSE.txt.erb +21 -21
  112. data/lib/theme_template/README.md.erb +50 -52
  113. data/lib/theme_template/_layouts/default.html +1 -1
  114. data/lib/theme_template/_layouts/page.html +5 -5
  115. data/lib/theme_template/_layouts/post.html +5 -5
  116. data/lib/theme_template/example/_config.yml.erb +1 -1
  117. data/lib/theme_template/example/_post.md +12 -12
  118. data/lib/theme_template/example/index.html +14 -14
  119. data/lib/theme_template/example/style.scss +7 -7
  120. data/lib/theme_template/gitignore.erb +6 -6
  121. data/lib/theme_template/theme.gemspec.erb +16 -16
  122. data/rubocop/jekyll/assert_equal_literal_actual.rb +149 -149
  123. data/rubocop/jekyll/no_p_allowed.rb +23 -23
  124. data/rubocop/jekyll/no_puts_allowed.rb +23 -23
  125. data/rubocop/jekyll.rb +5 -5
  126. metadata +64 -18
data/lib/jekyll/utils.rb CHANGED
@@ -1,367 +1,371 @@
1
- # frozen_string_literal: true
2
-
3
- module Jekyll
4
- module Utils
5
- extend self
6
- autoload :Ansi, "jekyll/utils/ansi"
7
- autoload :Exec, "jekyll/utils/exec"
8
- autoload :Internet, "jekyll/utils/internet"
9
- autoload :Platforms, "jekyll/utils/platforms"
10
- autoload :ThreadEvent, "jekyll/utils/thread_event"
11
- autoload :WinTZ, "jekyll/utils/win_tz"
12
-
13
- # Constants for use in #slugify
14
- SLUGIFY_MODES = %w(raw default pretty ascii latin).freeze
15
- SLUGIFY_RAW_REGEXP = Regexp.new('\\s+').freeze
16
- SLUGIFY_DEFAULT_REGEXP = Regexp.new("[^\\p{M}\\p{L}\\p{Nd}]+").freeze
17
- SLUGIFY_PRETTY_REGEXP = Regexp.new("[^\\p{M}\\p{L}\\p{Nd}._~!$&'()+,;=@]+").freeze
18
- SLUGIFY_ASCII_REGEXP = Regexp.new("[^[A-Za-z0-9]]+").freeze
19
-
20
- # Takes a slug and turns it into a simple title.
21
- def titleize_slug(slug)
22
- slug.split("-").map!(&:capitalize).join(" ")
23
- end
24
-
25
- # Non-destructive version of deep_merge_hashes! See that method.
26
- #
27
- # Returns the merged hashes.
28
- def deep_merge_hashes(master_hash, other_hash)
29
- deep_merge_hashes!(master_hash.dup, other_hash)
30
- end
31
-
32
- # Merges a master hash with another hash, recursively.
33
- #
34
- # master_hash - the "parent" hash whose values will be overridden
35
- # other_hash - the other hash whose values will be persisted after the merge
36
- #
37
- # This code was lovingly stolen from some random gem:
38
- # http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
39
- #
40
- # Thanks to whoever made it.
41
- def deep_merge_hashes!(target, overwrite)
42
- merge_values(target, overwrite)
43
- merge_default_proc(target, overwrite)
44
- duplicate_frozen_values(target)
45
-
46
- target
47
- end
48
-
49
- def mergable?(value)
50
- value.is_a?(Hash) || value.is_a?(Drops::Drop)
51
- end
52
-
53
- def duplicable?(obj)
54
- case obj
55
- when nil, false, true, Symbol, Numeric
56
- false
57
- else
58
- true
59
- end
60
- end
61
-
62
- # Read array from the supplied hash favouring the singular key
63
- # and then the plural key, and handling any nil entries.
64
- #
65
- # hash - the hash to read from
66
- # singular_key - the singular key
67
- # plural_key - the plural key
68
- #
69
- # Returns an array
70
- def pluralized_array_from_hash(hash, singular_key, plural_key)
71
- array = []
72
- value = value_from_singular_key(hash, singular_key)
73
- value ||= value_from_plural_key(hash, plural_key)
74
-
75
- array << value
76
- array.flatten!
77
- array.compact!
78
- array
79
- end
80
-
81
- def value_from_singular_key(hash, key)
82
- hash[key] if hash.key?(key) || (hash.default_proc && hash[key])
83
- end
84
-
85
- def value_from_plural_key(hash, key)
86
- if hash.key?(key) || (hash.default_proc && hash[key])
87
- val = hash[key]
88
- case val
89
- when String
90
- val.split
91
- when Array
92
- val.compact
93
- end
94
- end
95
- end
96
-
97
- def transform_keys(hash)
98
- result = {}
99
- hash.each_key do |key|
100
- result[yield(key)] = hash[key]
101
- end
102
- result
103
- end
104
-
105
- # Apply #to_sym to all keys in the hash
106
- #
107
- # hash - the hash to which to apply this transformation
108
- #
109
- # Returns a new hash with symbolized keys
110
- def symbolize_hash_keys(hash)
111
- transform_keys(hash) { |key| key.to_sym rescue key }
112
- end
113
-
114
- # Apply #to_s to all keys in the Hash
115
- #
116
- # hash - the hash to which to apply this transformation
117
- #
118
- # Returns a new hash with stringified keys
119
- def stringify_hash_keys(hash)
120
- transform_keys(hash) { |key| key.to_s rescue key }
121
- end
122
-
123
- # Parse a date/time and throw an error if invalid
124
- #
125
- # input - the date/time to parse
126
- # msg - (optional) the error message to show the user
127
- #
128
- # Returns the parsed date if successful, throws a FatalException
129
- # if not
130
- def parse_date(input, msg = "Input could not be parsed.")
131
- Time.parse(input).localtime
132
- rescue ArgumentError
133
- raise Errors::InvalidDateError, "Invalid date '#{input}': #{msg}"
134
- end
135
-
136
- # Determines whether a given file has
137
- #
138
- # Returns true if the YAML front matter is present.
139
- # rubocop: disable Naming/PredicateName
140
- def has_yaml_header?(file)
141
- File.open(file, "rb", &:readline).match? %r!\A---\s*\r?\n!
142
- rescue EOFError
143
- false
144
- end
145
-
146
- # Determine whether the given content string contains Liquid Tags or Vaiables
147
- #
148
- # Returns true is the string contains sequences of `{%` or `{{`
149
- def has_liquid_construct?(content)
150
- return false if content.nil? || content.empty?
151
-
152
- content.include?("{%") || content.include?("{{")
153
- end
154
- # rubocop: enable Naming/PredicateName
155
-
156
- # Slugify a filename or title.
157
- #
158
- # string - the filename or title to slugify
159
- # mode - how string is slugified
160
- # cased - whether to replace all uppercase letters with their
161
- # lowercase counterparts
162
- #
163
- # When mode is "none", return the given string.
164
- #
165
- # When mode is "raw", return the given string,
166
- # with every sequence of spaces characters replaced with a hyphen.
167
- #
168
- # When mode is "default" or nil, non-alphabetic characters are
169
- # replaced with a hyphen too.
170
- #
171
- # When mode is "pretty", some non-alphabetic characters (._~!$&'()+,;=@)
172
- # are not replaced with hyphen.
173
- #
174
- # When mode is "ascii", some everything else except ASCII characters
175
- # a-z (lowercase), A-Z (uppercase) and 0-9 (numbers) are not replaced with hyphen.
176
- #
177
- # When mode is "latin", the input string is first preprocessed so that
178
- # any letters with accents are replaced with the plain letter. Afterwards,
179
- # it follows the "default" mode of operation.
180
- #
181
- # If cased is true, all uppercase letters in the result string are
182
- # replaced with their lowercase counterparts.
183
- #
184
- # Examples:
185
- # slugify("The _config.yml file")
186
- # # => "the-config-yml-file"
187
- #
188
- # slugify("The _config.yml file", "pretty")
189
- # # => "the-_config.yml-file"
190
- #
191
- # slugify("The _config.yml file", "pretty", true)
192
- # # => "The-_config.yml file"
193
- #
194
- # slugify("The _config.yml file", "ascii")
195
- # # => "the-config-yml-file"
196
- #
197
- # slugify("The _config.yml file", "latin")
198
- # # => "the-config-yml-file"
199
- #
200
- # Returns the slugified string.
201
- def slugify(string, mode: nil, cased: false)
202
- mode ||= "default"
203
- return nil if string.nil?
204
-
205
- unless SLUGIFY_MODES.include?(mode)
206
- return cased ? string : string.downcase
207
- end
208
-
209
- # Drop accent marks from latin characters. Everything else turns to ?
210
- if mode == "latin"
211
- I18n.config.available_locales = :en if I18n.config.available_locales.empty?
212
- string = I18n.transliterate(string)
213
- end
214
-
215
- slug = replace_character_sequence_with_hyphen(string, :mode => mode)
216
-
217
- # Remove leading/trailing hyphen
218
- slug.gsub!(%r!^-|-$!i, "")
219
-
220
- slug.downcase! unless cased
221
- Jekyll.logger.warn("Warning:", "Empty `slug` generated for '#{string}'.") if slug.empty?
222
- slug
223
- end
224
-
225
- # Add an appropriate suffix to template so that it matches the specified
226
- # permalink style.
227
- #
228
- # template - permalink template without trailing slash or file extension
229
- # permalink_style - permalink style, either built-in or custom
230
- #
231
- # The returned permalink template will use the same ending style as
232
- # specified in permalink_style. For example, if permalink_style contains a
233
- # trailing slash (or is :pretty, which indirectly has a trailing slash),
234
- # then so will the returned template. If permalink_style has a trailing
235
- # ":output_ext" (or is :none, :date, or :ordinal) then so will the returned
236
- # template. Otherwise, template will be returned without modification.
237
- #
238
- # Examples:
239
- # add_permalink_suffix("/:basename", :pretty)
240
- # # => "/:basename/"
241
- #
242
- # add_permalink_suffix("/:basename", :date)
243
- # # => "/:basename:output_ext"
244
- #
245
- # add_permalink_suffix("/:basename", "/:year/:month/:title/")
246
- # # => "/:basename/"
247
- #
248
- # add_permalink_suffix("/:basename", "/:year/:month/:title")
249
- # # => "/:basename"
250
- #
251
- # Returns the updated permalink template
252
- def add_permalink_suffix(template, permalink_style)
253
- template = template.dup
254
-
255
- case permalink_style
256
- when :pretty
257
- template << "/"
258
- when :date, :ordinal, :none
259
- template << ":output_ext"
260
- else
261
- template << "/" if permalink_style.to_s.end_with?("/")
262
- template << ":output_ext" if permalink_style.to_s.end_with?(":output_ext")
263
- end
264
-
265
- template
266
- end
267
-
268
- # Work the same way as Dir.glob but seperating the input into two parts
269
- # ('dir' + '/' + 'pattern') to make sure the first part('dir') does not act
270
- # as a pattern.
271
- #
272
- # For example, Dir.glob("path[/*") always returns an empty array,
273
- # because the method fails to find the closing pattern to '[' which is ']'
274
- #
275
- # Examples:
276
- # safe_glob("path[", "*")
277
- # # => ["path[/file1", "path[/file2"]
278
- #
279
- # safe_glob("path", "*", File::FNM_DOTMATCH)
280
- # # => ["path/.", "path/..", "path/file1"]
281
- #
282
- # safe_glob("path", ["**", "*"])
283
- # # => ["path[/file1", "path[/folder/file2"]
284
- #
285
- # dir - the dir where glob will be executed under
286
- # (the dir will be included to each result)
287
- # patterns - the patterns (or the pattern) which will be applied under the dir
288
- # flags - the flags which will be applied to the pattern
289
- #
290
- # Returns matched pathes
291
- def safe_glob(dir, patterns, flags = 0)
292
- return [] unless Dir.exist?(dir)
293
-
294
- pattern = File.join(Array(patterns))
295
- return [dir] if pattern.empty?
296
-
297
- Dir.chdir(dir) do
298
- Dir.glob(pattern, flags).map { |f| File.join(dir, f) }
299
- end
300
- end
301
-
302
- # Returns merged option hash for File.read of self.site (if exists)
303
- # and a given param
304
- def merged_file_read_opts(site, opts)
305
- merged = (site ? site.file_read_opts : {}).merge(opts)
306
- if merged[:encoding] && !merged[:encoding].start_with?("bom|")
307
- merged[:encoding] = "bom|#{merged[:encoding]}"
308
- end
309
- if merged["encoding"] && !merged["encoding"].start_with?("bom|")
310
- merged["encoding"] = "bom|#{merged["encoding"]}"
311
- end
312
- merged
313
- end
314
-
315
- private
316
-
317
- def merge_values(target, overwrite)
318
- target.merge!(overwrite) do |_key, old_val, new_val|
319
- if new_val.nil?
320
- old_val
321
- elsif mergable?(old_val) && mergable?(new_val)
322
- deep_merge_hashes(old_val, new_val)
323
- else
324
- new_val
325
- end
326
- end
327
- end
328
-
329
- def merge_default_proc(target, overwrite)
330
- if target.is_a?(Hash) && overwrite.is_a?(Hash) && target.default_proc.nil?
331
- target.default_proc = overwrite.default_proc
332
- end
333
- end
334
-
335
- def duplicate_frozen_values(target)
336
- target.each do |key, val|
337
- target[key] = val.dup if val.frozen? && duplicable?(val)
338
- end
339
- end
340
-
341
- # Replace each character sequence with a hyphen.
342
- #
343
- # See Utils#slugify for a description of the character sequence specified
344
- # by each mode.
345
- def replace_character_sequence_with_hyphen(string, mode: "default")
346
- replaceable_char =
347
- case mode
348
- when "raw"
349
- SLUGIFY_RAW_REGEXP
350
- when "pretty"
351
- # "._~!$&'()+,;=@" is human readable (not URI-escaped) in URL
352
- # and is allowed in both extN and NTFS.
353
- SLUGIFY_PRETTY_REGEXP
354
- when "ascii"
355
- # For web servers not being able to handle Unicode, the safe
356
- # method is to ditch anything else but latin letters and numeric
357
- # digits.
358
- SLUGIFY_ASCII_REGEXP
359
- else
360
- SLUGIFY_DEFAULT_REGEXP
361
- end
362
-
363
- # Strip according to the mode
364
- string.gsub(replaceable_char, "-")
365
- end
366
- end
367
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Utils
5
+ extend self
6
+ autoload :Ansi, "jekyll/utils/ansi"
7
+ autoload :Exec, "jekyll/utils/exec"
8
+ autoload :Internet, "jekyll/utils/internet"
9
+ autoload :Platforms, "jekyll/utils/platforms"
10
+ autoload :ThreadEvent, "jekyll/utils/thread_event"
11
+ autoload :WinTZ, "jekyll/utils/win_tz"
12
+
13
+ # Constants for use in #slugify
14
+ SLUGIFY_MODES = %w(raw default pretty ascii latin).freeze
15
+ SLUGIFY_RAW_REGEXP = Regexp.new('\\s+').freeze
16
+ SLUGIFY_DEFAULT_REGEXP = Regexp.new("[^\\p{M}\\p{L}\\p{Nd}]+").freeze
17
+ SLUGIFY_PRETTY_REGEXP = Regexp.new("[^\\p{M}\\p{L}\\p{Nd}._~!$&'()+,;=@]+").freeze
18
+ SLUGIFY_ASCII_REGEXP = Regexp.new("[^[A-Za-z0-9]]+").freeze
19
+
20
+ # Takes a slug and turns it into a simple title.
21
+ def titleize_slug(slug)
22
+ slug.split("-").map!(&:capitalize).join(" ")
23
+ end
24
+
25
+ # Non-destructive version of deep_merge_hashes! See that method.
26
+ #
27
+ # Returns the merged hashes.
28
+ def deep_merge_hashes(master_hash, other_hash)
29
+ deep_merge_hashes!(master_hash.dup, other_hash)
30
+ end
31
+
32
+ # Merges a master hash with another hash, recursively.
33
+ #
34
+ # master_hash - the "parent" hash whose values will be overridden
35
+ # other_hash - the other hash whose values will be persisted after the merge
36
+ #
37
+ # This code was lovingly stolen from some random gem:
38
+ # http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
39
+ #
40
+ # Thanks to whoever made it.
41
+ def deep_merge_hashes!(target, overwrite)
42
+ merge_values(target, overwrite)
43
+ merge_default_proc(target, overwrite)
44
+ duplicate_frozen_values(target)
45
+
46
+ target
47
+ end
48
+
49
+ def mergable?(value)
50
+ value.is_a?(Hash) || value.is_a?(Drops::Drop)
51
+ end
52
+
53
+ def duplicable?(obj)
54
+ case obj
55
+ when nil, false, true, Symbol, Numeric
56
+ false
57
+ else
58
+ true
59
+ end
60
+ end
61
+
62
+ # Read array from the supplied hash favouring the singular key
63
+ # and then the plural key, and handling any nil entries.
64
+ #
65
+ # hash - the hash to read from
66
+ # singular_key - the singular key
67
+ # plural_key - the plural key
68
+ #
69
+ # Returns an array
70
+ def pluralized_array_from_hash(hash, singular_key, plural_key)
71
+ array = []
72
+ value = value_from_singular_key(hash, singular_key)
73
+ value ||= value_from_plural_key(hash, plural_key)
74
+
75
+ array << value
76
+ array.flatten!
77
+ array.compact!
78
+ array
79
+ end
80
+
81
+ def value_from_singular_key(hash, key)
82
+ hash[key] if hash.key?(key) || (hash.default_proc && hash[key])
83
+ end
84
+
85
+ def value_from_plural_key(hash, key)
86
+ if hash.key?(key) || (hash.default_proc && hash[key])
87
+ val = hash[key]
88
+ case val
89
+ when String
90
+ val.split
91
+ when Array
92
+ val.compact
93
+ end
94
+ end
95
+ end
96
+
97
+ def transform_keys(hash)
98
+ result = {}
99
+ hash.each_key do |key|
100
+ result[yield(key)] = hash[key]
101
+ end
102
+ result
103
+ end
104
+
105
+ # Apply #to_sym to all keys in the hash
106
+ #
107
+ # hash - the hash to which to apply this transformation
108
+ #
109
+ # Returns a new hash with symbolized keys
110
+ def symbolize_hash_keys(hash)
111
+ transform_keys(hash) { |key| key.to_sym rescue key }
112
+ end
113
+
114
+ # Apply #to_s to all keys in the Hash
115
+ #
116
+ # hash - the hash to which to apply this transformation
117
+ #
118
+ # Returns a new hash with stringified keys
119
+ def stringify_hash_keys(hash)
120
+ transform_keys(hash) { |key| key.to_s rescue key }
121
+ end
122
+
123
+ # Parse a date/time and throw an error if invalid
124
+ #
125
+ # input - the date/time to parse
126
+ # msg - (optional) the error message to show the user
127
+ #
128
+ # Returns the parsed date if successful, throws a FatalException
129
+ # if not
130
+ def parse_date(input, msg = "Input could not be parsed.")
131
+ @parse_date_cache ||= {}
132
+ @parse_date_cache[input] ||= Time.parse(input).localtime
133
+ rescue ArgumentError
134
+ raise Errors::InvalidDateError, "Invalid date '#{input}': #{msg}"
135
+ end
136
+
137
+ # Determines whether a given file has
138
+ #
139
+ # Returns true if the YAML front matter is present.
140
+ # rubocop: disable Naming/PredicateName
141
+ def has_yaml_header?(file)
142
+ File.open(file, "rb", &:readline).match? %r!\A---\s*\r?\n!
143
+ rescue EOFError
144
+ false
145
+ end
146
+
147
+ # Determine whether the given content string contains Liquid Tags or Variables
148
+ #
149
+ # Returns true is the string contains sequences of `{%` or `{{`
150
+ def has_liquid_construct?(content)
151
+ return false if content.nil? || content.empty?
152
+
153
+ content.include?("{%") || content.include?("{{")
154
+ end
155
+ # rubocop: enable Naming/PredicateName
156
+
157
+ # Slugify a filename or title.
158
+ #
159
+ # string - the filename or title to slugify
160
+ # mode - how string is slugified
161
+ # cased - whether to replace all uppercase letters with their
162
+ # lowercase counterparts
163
+ #
164
+ # When mode is "none", return the given string.
165
+ #
166
+ # When mode is "raw", return the given string,
167
+ # with every sequence of spaces characters replaced with a hyphen.
168
+ #
169
+ # When mode is "default" or nil, non-alphabetic characters are
170
+ # replaced with a hyphen too.
171
+ #
172
+ # When mode is "pretty", some non-alphabetic characters (._~!$&'()+,;=@)
173
+ # are not replaced with hyphen.
174
+ #
175
+ # When mode is "ascii", some everything else except ASCII characters
176
+ # a-z (lowercase), A-Z (uppercase) and 0-9 (numbers) are not replaced with hyphen.
177
+ #
178
+ # When mode is "latin", the input string is first preprocessed so that
179
+ # any letters with accents are replaced with the plain letter. Afterwards,
180
+ # it follows the "default" mode of operation.
181
+ #
182
+ # If cased is true, all uppercase letters in the result string are
183
+ # replaced with their lowercase counterparts.
184
+ #
185
+ # Examples:
186
+ # slugify("The _config.yml file")
187
+ # # => "the-config-yml-file"
188
+ #
189
+ # slugify("The _config.yml file", "pretty")
190
+ # # => "the-_config.yml-file"
191
+ #
192
+ # slugify("The _config.yml file", "pretty", true)
193
+ # # => "The-_config.yml file"
194
+ #
195
+ # slugify("The _config.yml file", "ascii")
196
+ # # => "the-config-yml-file"
197
+ #
198
+ # slugify("The _config.yml file", "latin")
199
+ # # => "the-config-yml-file"
200
+ #
201
+ # Returns the slugified string.
202
+ def slugify(string, mode: nil, cased: false)
203
+ mode ||= "default"
204
+ return nil if string.nil?
205
+
206
+ unless SLUGIFY_MODES.include?(mode)
207
+ return cased ? string : string.downcase
208
+ end
209
+
210
+ # Drop accent marks from latin characters. Everything else turns to ?
211
+ if mode == "latin"
212
+ I18n.config.available_locales = :en if I18n.config.available_locales.empty?
213
+ string = I18n.transliterate(string)
214
+ end
215
+
216
+ slug = replace_character_sequence_with_hyphen(string, :mode => mode)
217
+
218
+ # Remove leading/trailing hyphen
219
+ slug.gsub!(%r!^-|-$!i, "")
220
+
221
+ slug.downcase! unless cased
222
+ Jekyll.logger.warn("Warning:", "Empty `slug` generated for '#{string}'.") if slug.empty?
223
+ slug
224
+ end
225
+
226
+ # Add an appropriate suffix to template so that it matches the specified
227
+ # permalink style.
228
+ #
229
+ # template - permalink template without trailing slash or file extension
230
+ # permalink_style - permalink style, either built-in or custom
231
+ #
232
+ # The returned permalink template will use the same ending style as
233
+ # specified in permalink_style. For example, if permalink_style contains a
234
+ # trailing slash (or is :pretty, which indirectly has a trailing slash),
235
+ # then so will the returned template. If permalink_style has a trailing
236
+ # ":output_ext" (or is :none, :date, or :ordinal) then so will the returned
237
+ # template. Otherwise, template will be returned without modification.
238
+ #
239
+ # Examples:
240
+ # add_permalink_suffix("/:basename", :pretty)
241
+ # # => "/:basename/"
242
+ #
243
+ # add_permalink_suffix("/:basename", :date)
244
+ # # => "/:basename:output_ext"
245
+ #
246
+ # add_permalink_suffix("/:basename", "/:year/:month/:title/")
247
+ # # => "/:basename/"
248
+ #
249
+ # add_permalink_suffix("/:basename", "/:year/:month/:title")
250
+ # # => "/:basename"
251
+ #
252
+ # Returns the updated permalink template
253
+ def add_permalink_suffix(template, permalink_style)
254
+ template = template.dup
255
+
256
+ case permalink_style
257
+ when :pretty
258
+ template << "/"
259
+ when :date, :ordinal, :none
260
+ template << ":output_ext"
261
+ else
262
+ template << "/" if permalink_style.to_s.end_with?("/")
263
+ template << ":output_ext" if permalink_style.to_s.end_with?(":output_ext")
264
+ end
265
+
266
+ template
267
+ end
268
+
269
+ # Work the same way as Dir.glob but separating the input into two parts
270
+ # ('dir' + '/' + 'pattern') to make sure the first part('dir') does not act
271
+ # as a pattern.
272
+ #
273
+ # For example, Dir.glob("path[/*") always returns an empty array,
274
+ # because the method fails to find the closing pattern to '[' which is ']'
275
+ #
276
+ # Examples:
277
+ # safe_glob("path[", "*")
278
+ # # => ["path[/file1", "path[/file2"]
279
+ #
280
+ # safe_glob("path", "*", File::FNM_DOTMATCH)
281
+ # # => ["path/.", "path/..", "path/file1"]
282
+ #
283
+ # safe_glob("path", ["**", "*"])
284
+ # # => ["path[/file1", "path[/folder/file2"]
285
+ #
286
+ # dir - the dir where glob will be executed under
287
+ # (the dir will be included to each result)
288
+ # patterns - the patterns (or the pattern) which will be applied under the dir
289
+ # flags - the flags which will be applied to the pattern
290
+ #
291
+ # Returns matched paths
292
+ def safe_glob(dir, patterns, flags = 0)
293
+ return [] unless Dir.exist?(dir)
294
+
295
+ pattern = File.join(Array(patterns))
296
+ return [dir] if pattern.empty?
297
+
298
+ Dir.chdir(dir) do
299
+ Dir.glob(pattern, flags).map { |f| File.join(dir, f) }
300
+ end
301
+ end
302
+
303
+ # Returns merged option hash for File.read of self.site (if exists)
304
+ # and a given param
305
+ def merged_file_read_opts(site, opts)
306
+ merged = (site ? site.file_read_opts : {}).merge(opts)
307
+
308
+ # always use BOM when reading UTF-encoded files
309
+ if merged[:encoding]&.downcase&.start_with?("utf-")
310
+ merged[:encoding] = "bom|#{merged[:encoding]}"
311
+ end
312
+ if merged["encoding"]&.downcase&.start_with?("utf-")
313
+ merged["encoding"] = "bom|#{merged["encoding"]}"
314
+ end
315
+
316
+ merged
317
+ end
318
+
319
+ private
320
+
321
+ def merge_values(target, overwrite)
322
+ target.merge!(overwrite) do |_key, old_val, new_val|
323
+ if new_val.nil?
324
+ old_val
325
+ elsif mergable?(old_val) && mergable?(new_val)
326
+ deep_merge_hashes(old_val, new_val)
327
+ else
328
+ new_val
329
+ end
330
+ end
331
+ end
332
+
333
+ def merge_default_proc(target, overwrite)
334
+ if target.is_a?(Hash) && overwrite.is_a?(Hash) && target.default_proc.nil?
335
+ target.default_proc = overwrite.default_proc
336
+ end
337
+ end
338
+
339
+ def duplicate_frozen_values(target)
340
+ target.each do |key, val|
341
+ target[key] = val.dup if val.frozen? && duplicable?(val)
342
+ end
343
+ end
344
+
345
+ # Replace each character sequence with a hyphen.
346
+ #
347
+ # See Utils#slugify for a description of the character sequence specified
348
+ # by each mode.
349
+ def replace_character_sequence_with_hyphen(string, mode: "default")
350
+ replaceable_char =
351
+ case mode
352
+ when "raw"
353
+ SLUGIFY_RAW_REGEXP
354
+ when "pretty"
355
+ # "._~!$&'()+,;=@" is human readable (not URI-escaped) in URL
356
+ # and is allowed in both extN and NTFS.
357
+ SLUGIFY_PRETTY_REGEXP
358
+ when "ascii"
359
+ # For web servers not being able to handle Unicode, the safe
360
+ # method is to ditch anything else but latin letters and numeric
361
+ # digits.
362
+ SLUGIFY_ASCII_REGEXP
363
+ else
364
+ SLUGIFY_DEFAULT_REGEXP
365
+ end
366
+
367
+ # Strip according to the mode
368
+ string.gsub(replaceable_char, "-")
369
+ end
370
+ end
371
+ end