jekyll 4.2.1 → 4.3.0

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