jekyll 4.2.1 → 4.2.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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +350 -350
  3. data/LICENSE +21 -21
  4. data/README.markdown +86 -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 +190 -190
  12. data/lib/jekyll/cleaner.rb +111 -111
  13. data/lib/jekyll/collection.rb +309 -309
  14. data/lib/jekyll/command.rb +105 -105
  15. data/lib/jekyll/commands/build.rb +93 -93
  16. data/lib/jekyll/commands/clean.rb +45 -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 +172 -169
  20. data/lib/jekyll/commands/new_theme.rb +40 -40
  21. data/lib/jekyll/commands/serve/live_reload_reactor.rb +122 -122
  22. data/lib/jekyll/commands/serve/livereload_assets/livereload.js +1183 -1183
  23. data/lib/jekyll/commands/serve/servlet.rb +202 -202
  24. data/lib/jekyll/commands/serve/websockets.rb +81 -81
  25. data/lib/jekyll/commands/serve.rb +362 -362
  26. data/lib/jekyll/configuration.rb +313 -313
  27. data/lib/jekyll/converter.rb +54 -54
  28. data/lib/jekyll/converters/identity.rb +41 -41
  29. data/lib/jekyll/converters/markdown/kramdown_parser.rb +199 -199
  30. data/lib/jekyll/converters/markdown.rb +113 -113
  31. data/lib/jekyll/converters/smartypants.rb +70 -70
  32. data/lib/jekyll/convertible.rb +257 -257
  33. data/lib/jekyll/deprecator.rb +50 -50
  34. data/lib/jekyll/document.rb +544 -544
  35. data/lib/jekyll/drops/collection_drop.rb +20 -20
  36. data/lib/jekyll/drops/document_drop.rb +70 -70
  37. data/lib/jekyll/drops/drop.rb +293 -293
  38. data/lib/jekyll/drops/excerpt_drop.rb +19 -19
  39. data/lib/jekyll/drops/jekyll_drop.rb +32 -32
  40. data/lib/jekyll/drops/site_drop.rb +66 -66
  41. data/lib/jekyll/drops/static_file_drop.rb +14 -14
  42. data/lib/jekyll/drops/unified_payload_drop.rb +26 -26
  43. data/lib/jekyll/drops/url_drop.rb +140 -140
  44. data/lib/jekyll/entry_filter.rb +121 -121
  45. data/lib/jekyll/errors.rb +20 -20
  46. data/lib/jekyll/excerpt.rb +201 -201
  47. data/lib/jekyll/external.rb +79 -79
  48. data/lib/jekyll/filters/date_filters.rb +110 -110
  49. data/lib/jekyll/filters/grouping_filters.rb +64 -64
  50. data/lib/jekyll/filters/url_filters.rb +98 -98
  51. data/lib/jekyll/filters.rb +535 -535
  52. data/lib/jekyll/frontmatter_defaults.rb +240 -240
  53. data/lib/jekyll/generator.rb +5 -5
  54. data/lib/jekyll/hooks.rb +107 -107
  55. data/lib/jekyll/inclusion.rb +32 -32
  56. data/lib/jekyll/layout.rb +67 -67
  57. data/lib/jekyll/liquid_extensions.rb +22 -22
  58. data/lib/jekyll/liquid_renderer/file.rb +77 -77
  59. data/lib/jekyll/liquid_renderer/table.rb +55 -55
  60. data/lib/jekyll/liquid_renderer.rb +80 -80
  61. data/lib/jekyll/log_adapter.rb +151 -151
  62. data/lib/jekyll/mime.types +866 -866
  63. data/lib/jekyll/page.rb +217 -217
  64. data/lib/jekyll/page_excerpt.rb +25 -25
  65. data/lib/jekyll/page_without_a_file.rb +14 -14
  66. data/lib/jekyll/path_manager.rb +74 -74
  67. data/lib/jekyll/plugin.rb +92 -92
  68. data/lib/jekyll/plugin_manager.rb +115 -115
  69. data/lib/jekyll/profiler.rb +58 -58
  70. data/lib/jekyll/publisher.rb +23 -23
  71. data/lib/jekyll/reader.rb +192 -192
  72. data/lib/jekyll/readers/collection_reader.rb +23 -23
  73. data/lib/jekyll/readers/data_reader.rb +79 -79
  74. data/lib/jekyll/readers/layout_reader.rb +62 -62
  75. data/lib/jekyll/readers/page_reader.rb +25 -25
  76. data/lib/jekyll/readers/post_reader.rb +85 -85
  77. data/lib/jekyll/readers/static_file_reader.rb +25 -25
  78. data/lib/jekyll/readers/theme_assets_reader.rb +52 -52
  79. data/lib/jekyll/regenerator.rb +195 -195
  80. data/lib/jekyll/related_posts.rb +52 -52
  81. data/lib/jekyll/renderer.rb +265 -265
  82. data/lib/jekyll/site.rb +551 -551
  83. data/lib/jekyll/static_file.rb +208 -208
  84. data/lib/jekyll/stevenson.rb +60 -60
  85. data/lib/jekyll/tags/highlight.rb +110 -110
  86. data/lib/jekyll/tags/include.rb +275 -275
  87. data/lib/jekyll/tags/link.rb +42 -42
  88. data/lib/jekyll/tags/post_url.rb +106 -106
  89. data/lib/jekyll/theme.rb +86 -86
  90. data/lib/jekyll/theme_builder.rb +121 -121
  91. data/lib/jekyll/url.rb +167 -167
  92. data/lib/jekyll/utils/ansi.rb +57 -57
  93. data/lib/jekyll/utils/exec.rb +26 -26
  94. data/lib/jekyll/utils/internet.rb +37 -37
  95. data/lib/jekyll/utils/platforms.rb +67 -67
  96. data/lib/jekyll/utils/thread_event.rb +31 -31
  97. data/lib/jekyll/utils/win_tz.rb +75 -75
  98. data/lib/jekyll/utils.rb +367 -367
  99. data/lib/jekyll/version.rb +5 -5
  100. data/lib/jekyll.rb +195 -195
  101. data/lib/site_template/.gitignore +5 -5
  102. data/lib/site_template/404.html +25 -25
  103. data/lib/site_template/_config.yml +55 -55
  104. data/lib/site_template/_posts/0000-00-00-welcome-to-jekyll.markdown.erb +29 -29
  105. data/lib/site_template/about.markdown +18 -18
  106. data/lib/site_template/index.markdown +6 -6
  107. data/lib/theme_template/CODE_OF_CONDUCT.md.erb +74 -74
  108. data/lib/theme_template/Gemfile +4 -4
  109. data/lib/theme_template/LICENSE.txt.erb +21 -21
  110. data/lib/theme_template/README.md.erb +52 -52
  111. data/lib/theme_template/_layouts/default.html +1 -1
  112. data/lib/theme_template/_layouts/page.html +5 -5
  113. data/lib/theme_template/_layouts/post.html +5 -5
  114. data/lib/theme_template/example/_config.yml.erb +1 -1
  115. data/lib/theme_template/example/_post.md +12 -12
  116. data/lib/theme_template/example/index.html +14 -14
  117. data/lib/theme_template/example/style.scss +7 -7
  118. data/lib/theme_template/gitignore.erb +6 -6
  119. data/lib/theme_template/theme.gemspec.erb +16 -16
  120. data/rubocop/jekyll/assert_equal_literal_actual.rb +149 -149
  121. data/rubocop/jekyll/no_p_allowed.rb +23 -23
  122. data/rubocop/jekyll/no_puts_allowed.rb +23 -23
  123. data/rubocop/jekyll.rb +5 -5
  124. metadata +3 -3
@@ -1,535 +1,535 @@
1
- # frozen_string_literal: true
2
-
3
- require_all "jekyll/filters"
4
-
5
- module Jekyll
6
- module Filters
7
- include URLFilters
8
- include GroupingFilters
9
- include DateFilters
10
-
11
- # Convert a Markdown string into HTML output.
12
- #
13
- # input - The Markdown String to convert.
14
- #
15
- # Returns the HTML formatted String.
16
- def markdownify(input)
17
- @context.registers[:site].find_converter_instance(
18
- Jekyll::Converters::Markdown
19
- ).convert(input.to_s)
20
- end
21
-
22
- # Convert quotes into smart quotes.
23
- #
24
- # input - The String to convert.
25
- #
26
- # Returns the smart-quotified String.
27
- def smartify(input)
28
- @context.registers[:site].find_converter_instance(
29
- Jekyll::Converters::SmartyPants
30
- ).convert(input.to_s)
31
- end
32
-
33
- # Convert a Sass string into CSS output.
34
- #
35
- # input - The Sass String to convert.
36
- #
37
- # Returns the CSS formatted String.
38
- def sassify(input)
39
- @context.registers[:site].find_converter_instance(
40
- Jekyll::Converters::Sass
41
- ).convert(input)
42
- end
43
-
44
- # Convert a Scss string into CSS output.
45
- #
46
- # input - The Scss String to convert.
47
- #
48
- # Returns the CSS formatted String.
49
- def scssify(input)
50
- @context.registers[:site].find_converter_instance(
51
- Jekyll::Converters::Scss
52
- ).convert(input)
53
- end
54
-
55
- # Slugify a filename or title.
56
- #
57
- # input - The filename or title to slugify.
58
- # mode - how string is slugified
59
- #
60
- # Returns the given filename or title as a lowercase URL String.
61
- # See Utils.slugify for more detail.
62
- def slugify(input, mode = nil)
63
- Utils.slugify(input, :mode => mode)
64
- end
65
-
66
- # XML escape a string for use. Replaces any special characters with
67
- # appropriate HTML entity replacements.
68
- #
69
- # input - The String to escape.
70
- #
71
- # Examples
72
- #
73
- # xml_escape('foo "bar" <baz>')
74
- # # => "foo &quot;bar&quot; &lt;baz&gt;"
75
- #
76
- # Returns the escaped String.
77
- def xml_escape(input)
78
- input.to_s.encode(:xml => :attr).gsub(%r!\A"|"\Z!, "")
79
- end
80
-
81
- # CGI escape a string for use in a URL. Replaces any special characters
82
- # with appropriate %XX replacements.
83
- #
84
- # input - The String to escape.
85
- #
86
- # Examples
87
- #
88
- # cgi_escape('foo,bar;baz?')
89
- # # => "foo%2Cbar%3Bbaz%3F"
90
- #
91
- # Returns the escaped String.
92
- def cgi_escape(input)
93
- CGI.escape(input)
94
- end
95
-
96
- # URI escape a string.
97
- #
98
- # input - The String to escape.
99
- #
100
- # Examples
101
- #
102
- # uri_escape('foo, bar \\baz?')
103
- # # => "foo,%20bar%20%5Cbaz?"
104
- #
105
- # Returns the escaped String.
106
- def uri_escape(input)
107
- Addressable::URI.normalize_component(input)
108
- end
109
-
110
- # Replace any whitespace in the input string with a single space
111
- #
112
- # input - The String on which to operate.
113
- #
114
- # Returns the formatted String
115
- def normalize_whitespace(input)
116
- input.to_s.gsub(%r!\s+!, " ").tap(&:strip!)
117
- end
118
-
119
- # Count the number of words in the input string.
120
- #
121
- # input - The String on which to operate.
122
- #
123
- # Returns the Integer word count.
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
138
- end
139
-
140
- # Join an array of things into a string by separating with commas and the
141
- # word "and" for the last one.
142
- #
143
- # array - The Array of Strings to join.
144
- # connector - Word used to connect the last 2 items in the array
145
- #
146
- # Examples
147
- #
148
- # array_to_sentence_string(["apples", "oranges", "grapes"])
149
- # # => "apples, oranges, and grapes"
150
- #
151
- # Returns the formatted String.
152
- def array_to_sentence_string(array, connector = "and")
153
- case array.length
154
- when 0
155
- ""
156
- when 1
157
- array[0].to_s
158
- when 2
159
- "#{array[0]} #{connector} #{array[1]}"
160
- else
161
- "#{array[0...-1].join(", ")}, #{connector} #{array[-1]}"
162
- end
163
- end
164
-
165
- # Convert the input into json string
166
- #
167
- # input - The Array or Hash to be converted
168
- #
169
- # Returns the converted json string
170
- def jsonify(input)
171
- as_liquid(input).to_json
172
- end
173
-
174
- # Filter an array of objects
175
- #
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.
181
- #
182
- # Returns the filtered array of objects
183
- def where(input, property, value)
184
- return input if !property || value.is_a?(Array) || value.is_a?(Hash)
185
- return input unless input.respond_to?(:select)
186
-
187
- input = input.values if input.is_a?(Hash)
188
- input_id = input.hash
189
-
190
- # implement a hash based on method parameters to cache the end-result
191
- # for given parameters.
192
- @where_filter_cache ||= {}
193
- @where_filter_cache[input_id] ||= {}
194
- @where_filter_cache[input_id][property] ||= {}
195
-
196
- # stash or retrive results to return
197
- @where_filter_cache[input_id][property][value] ||= begin
198
- input.select do |object|
199
- compare_property_vs_target(item_property(object, property), value)
200
- end.to_a
201
- end
202
- end
203
-
204
- # Filters an array of objects against an expression
205
- #
206
- # input - the object array
207
- # variable - the variable to assign each item to in the expression
208
- # expression - a Liquid comparison expression passed in as a string
209
- #
210
- # Returns the filtered array of objects
211
- def where_exp(input, variable, expression)
212
- return input unless input.respond_to?(:select)
213
-
214
- input = input.values if input.is_a?(Hash) # FIXME
215
-
216
- condition = parse_condition(expression)
217
- @context.stack do
218
- input.select do |object|
219
- @context[variable] = object
220
- condition.evaluate(@context)
221
- end
222
- end || []
223
- end
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
-
285
- # Convert the input into integer
286
- #
287
- # input - the object string
288
- #
289
- # Returns the integer value
290
- def to_integer(input)
291
- return 1 if input == true
292
- return 0 if input == false
293
-
294
- input.to_i
295
- end
296
-
297
- # Sort an array of objects
298
- #
299
- # input - the object array
300
- # property - property within each object to filter by
301
- # nils ('first' | 'last') - nils appear before or after non-nil values
302
- #
303
- # Returns the filtered array of objects
304
- def sort(input, property = nil, nils = "first")
305
- raise ArgumentError, "Cannot sort a null object." if input.nil?
306
-
307
- if property.nil?
308
- input.sort
309
- else
310
- case nils
311
- when "first"
312
- order = - 1
313
- when "last"
314
- order = + 1
315
- else
316
- raise ArgumentError, "Invalid nils order: " \
317
- "'#{nils}' is not a valid nils order. It must be 'first' or 'last'."
318
- end
319
-
320
- sort_input(input, property, order)
321
- end
322
- end
323
-
324
- def pop(array, num = 1)
325
- return array unless array.is_a?(Array)
326
-
327
- num = Liquid::Utils.to_integer(num)
328
- new_ary = array.dup
329
- new_ary.pop(num)
330
- new_ary
331
- end
332
-
333
- def push(array, input)
334
- return array unless array.is_a?(Array)
335
-
336
- new_ary = array.dup
337
- new_ary.push(input)
338
- new_ary
339
- end
340
-
341
- def shift(array, num = 1)
342
- return array unless array.is_a?(Array)
343
-
344
- num = Liquid::Utils.to_integer(num)
345
- new_ary = array.dup
346
- new_ary.shift(num)
347
- new_ary
348
- end
349
-
350
- def unshift(array, input)
351
- return array unless array.is_a?(Array)
352
-
353
- new_ary = array.dup
354
- new_ary.unshift(input)
355
- new_ary
356
- end
357
-
358
- def sample(input, num = 1)
359
- return input unless input.respond_to?(:sample)
360
-
361
- num = Liquid::Utils.to_integer(num) rescue 1
362
- if num == 1
363
- input.sample
364
- else
365
- input.sample(num)
366
- end
367
- end
368
-
369
- # Convert an object into its String representation for debugging
370
- #
371
- # input - The Object to be converted
372
- #
373
- # Returns a String representation of the object.
374
- def inspect(input)
375
- xml_escape(input.inspect)
376
- end
377
-
378
- private
379
-
380
- # Sort the input Enumerable by the given property.
381
- # If the property doesn't exist, return the sort order respective of
382
- # which item doesn't have the property.
383
- # We also utilize the Schwartzian transform to make this more efficient.
384
- def sort_input(input, property, order)
385
- input.map { |item| [item_property(item, property), item] }
386
- .sort! do |a_info, b_info|
387
- a_property = a_info.first
388
- b_property = b_info.first
389
-
390
- if !a_property.nil? && b_property.nil?
391
- - order
392
- elsif a_property.nil? && !b_property.nil?
393
- + order
394
- else
395
- a_property <=> b_property || a_property.to_s <=> b_property.to_s
396
- end
397
- end
398
- .map!(&:last)
399
- end
400
-
401
- # `where` filter helper
402
- #
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
- def item_property(item, property)
425
- @item_property_cache ||= @context.registers[:site].filter_cache[:item_property] ||= {}
426
- @item_property_cache[property] ||= {}
427
- @item_property_cache[property][item] ||= begin
428
- property = property.to_s
429
- property = if item.respond_to?(:to_liquid)
430
- read_liquid_attribute(item.to_liquid, property)
431
- elsif item.respond_to?(:data)
432
- item.data[property]
433
- else
434
- item[property]
435
- end
436
-
437
- parse_sort_input(property)
438
- end
439
- end
440
-
441
- def read_liquid_attribute(liquid_data, property)
442
- return liquid_data[property] unless property.include?(".")
443
-
444
- property.split(".").reduce(liquid_data) do |data, key|
445
- data.respond_to?(:[]) && data[key]
446
- end
447
- end
448
-
449
- FLOAT_LIKE = %r!\A\s*-?(?:\d+\.?\d*|\.\d+)\s*\Z!.freeze
450
- INTEGER_LIKE = %r!\A\s*-?\d+\s*\Z!.freeze
451
- private_constant :FLOAT_LIKE, :INTEGER_LIKE
452
-
453
- # return numeric values as numbers for proper sorting
454
- def parse_sort_input(property)
455
- stringified = property.to_s
456
- return property.to_i if INTEGER_LIKE.match?(stringified)
457
- return property.to_f if FLOAT_LIKE.match?(stringified)
458
-
459
- property
460
- end
461
-
462
- def as_liquid(item)
463
- case item
464
- when Hash
465
- item.each_with_object({}) { |(k, v), result| result[as_liquid(k)] = as_liquid(v) }
466
- when Array
467
- item.map { |i| as_liquid(i) }
468
- else
469
- if item.respond_to?(:to_liquid)
470
- liquidated = item.to_liquid
471
- # prevent infinite recursion for simple types (which return `self`)
472
- if liquidated == item
473
- item
474
- else
475
- as_liquid(liquidated)
476
- end
477
- else
478
- item
479
- end
480
- end
481
- end
482
-
483
- # ----------- The following set of code was *adapted* from Liquid::If
484
- # ----------- ref: https://git.io/vp6K6
485
-
486
- # Parse a string to a Liquid Condition
487
- def parse_condition(exp)
488
- parser = Liquid::Parser.new(exp)
489
- condition = parse_binary_comparison(parser)
490
-
491
- parser.consume(:end_of_string)
492
- condition
493
- end
494
-
495
- # Generate a Liquid::Condition object from a Liquid::Parser object additionally processing
496
- # the parsed expression based on whether the expression consists of binary operations with
497
- # Liquid operators `and` or `or`
498
- #
499
- # - parser: an instance of Liquid::Parser
500
- #
501
- # Returns an instance of Liquid::Condition
502
- def parse_binary_comparison(parser)
503
- condition = parse_comparison(parser)
504
- first_condition = condition
505
- while (binary_operator = parser.id?("and") || parser.id?("or"))
506
- child_condition = parse_comparison(parser)
507
- condition.send(binary_operator, child_condition)
508
- condition = child_condition
509
- end
510
- first_condition
511
- end
512
-
513
- # Generates a Liquid::Condition object from a Liquid::Parser object based on whether the parsed
514
- # expression involves a "comparison" operator (e.g. <, ==, >, !=, etc)
515
- #
516
- # - parser: an instance of Liquid::Parser
517
- #
518
- # Returns an instance of Liquid::Condition
519
- def parse_comparison(parser)
520
- left_operand = Liquid::Expression.parse(parser.expression)
521
- operator = parser.consume?(:comparison)
522
-
523
- # No comparison-operator detected. Initialize a Liquid::Condition using only left operand
524
- return Liquid::Condition.new(left_operand) unless operator
525
-
526
- # Parse what remained after extracting the left operand and the `:comparison` operator
527
- # and initialize a Liquid::Condition object using the operands and the comparison-operator
528
- Liquid::Condition.new(left_operand, operator, Liquid::Expression.parse(parser.expression))
529
- end
530
- end
531
- end
532
-
533
- Liquid::Template.register_filter(
534
- Jekyll::Filters
535
- )
1
+ # frozen_string_literal: true
2
+
3
+ require_all "jekyll/filters"
4
+
5
+ module Jekyll
6
+ module Filters
7
+ include URLFilters
8
+ include GroupingFilters
9
+ include DateFilters
10
+
11
+ # Convert a Markdown string into HTML output.
12
+ #
13
+ # input - The Markdown String to convert.
14
+ #
15
+ # Returns the HTML formatted String.
16
+ def markdownify(input)
17
+ @context.registers[:site].find_converter_instance(
18
+ Jekyll::Converters::Markdown
19
+ ).convert(input.to_s)
20
+ end
21
+
22
+ # Convert quotes into smart quotes.
23
+ #
24
+ # input - The String to convert.
25
+ #
26
+ # Returns the smart-quotified String.
27
+ def smartify(input)
28
+ @context.registers[:site].find_converter_instance(
29
+ Jekyll::Converters::SmartyPants
30
+ ).convert(input.to_s)
31
+ end
32
+
33
+ # Convert a Sass string into CSS output.
34
+ #
35
+ # input - The Sass String to convert.
36
+ #
37
+ # Returns the CSS formatted String.
38
+ def sassify(input)
39
+ @context.registers[:site].find_converter_instance(
40
+ Jekyll::Converters::Sass
41
+ ).convert(input)
42
+ end
43
+
44
+ # Convert a Scss string into CSS output.
45
+ #
46
+ # input - The Scss String to convert.
47
+ #
48
+ # Returns the CSS formatted String.
49
+ def scssify(input)
50
+ @context.registers[:site].find_converter_instance(
51
+ Jekyll::Converters::Scss
52
+ ).convert(input)
53
+ end
54
+
55
+ # Slugify a filename or title.
56
+ #
57
+ # input - The filename or title to slugify.
58
+ # mode - how string is slugified
59
+ #
60
+ # Returns the given filename or title as a lowercase URL String.
61
+ # See Utils.slugify for more detail.
62
+ def slugify(input, mode = nil)
63
+ Utils.slugify(input, :mode => mode)
64
+ end
65
+
66
+ # XML escape a string for use. Replaces any special characters with
67
+ # appropriate HTML entity replacements.
68
+ #
69
+ # input - The String to escape.
70
+ #
71
+ # Examples
72
+ #
73
+ # xml_escape('foo "bar" <baz>')
74
+ # # => "foo &quot;bar&quot; &lt;baz&gt;"
75
+ #
76
+ # Returns the escaped String.
77
+ def xml_escape(input)
78
+ input.to_s.encode(:xml => :attr).gsub(%r!\A"|"\Z!, "")
79
+ end
80
+
81
+ # CGI escape a string for use in a URL. Replaces any special characters
82
+ # with appropriate %XX replacements.
83
+ #
84
+ # input - The String to escape.
85
+ #
86
+ # Examples
87
+ #
88
+ # cgi_escape('foo,bar;baz?')
89
+ # # => "foo%2Cbar%3Bbaz%3F"
90
+ #
91
+ # Returns the escaped String.
92
+ def cgi_escape(input)
93
+ CGI.escape(input)
94
+ end
95
+
96
+ # URI escape a string.
97
+ #
98
+ # input - The String to escape.
99
+ #
100
+ # Examples
101
+ #
102
+ # uri_escape('foo, bar \\baz?')
103
+ # # => "foo,%20bar%20%5Cbaz?"
104
+ #
105
+ # Returns the escaped String.
106
+ def uri_escape(input)
107
+ Addressable::URI.normalize_component(input)
108
+ end
109
+
110
+ # Replace any whitespace in the input string with a single space
111
+ #
112
+ # input - The String on which to operate.
113
+ #
114
+ # Returns the formatted String
115
+ def normalize_whitespace(input)
116
+ input.to_s.gsub(%r!\s+!, " ").tap(&:strip!)
117
+ end
118
+
119
+ # Count the number of words in the input string.
120
+ #
121
+ # input - The String on which to operate.
122
+ #
123
+ # Returns the Integer word count.
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
138
+ end
139
+
140
+ # Join an array of things into a string by separating with commas and the
141
+ # word "and" for the last one.
142
+ #
143
+ # array - The Array of Strings to join.
144
+ # connector - Word used to connect the last 2 items in the array
145
+ #
146
+ # Examples
147
+ #
148
+ # array_to_sentence_string(["apples", "oranges", "grapes"])
149
+ # # => "apples, oranges, and grapes"
150
+ #
151
+ # Returns the formatted String.
152
+ def array_to_sentence_string(array, connector = "and")
153
+ case array.length
154
+ when 0
155
+ ""
156
+ when 1
157
+ array[0].to_s
158
+ when 2
159
+ "#{array[0]} #{connector} #{array[1]}"
160
+ else
161
+ "#{array[0...-1].join(", ")}, #{connector} #{array[-1]}"
162
+ end
163
+ end
164
+
165
+ # Convert the input into json string
166
+ #
167
+ # input - The Array or Hash to be converted
168
+ #
169
+ # Returns the converted json string
170
+ def jsonify(input)
171
+ as_liquid(input).to_json
172
+ end
173
+
174
+ # Filter an array of objects
175
+ #
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.
181
+ #
182
+ # Returns the filtered array of objects
183
+ def where(input, property, value)
184
+ return input if !property || value.is_a?(Array) || value.is_a?(Hash)
185
+ return input unless input.respond_to?(:select)
186
+
187
+ input = input.values if input.is_a?(Hash)
188
+ input_id = input.hash
189
+
190
+ # implement a hash based on method parameters to cache the end-result
191
+ # for given parameters.
192
+ @where_filter_cache ||= {}
193
+ @where_filter_cache[input_id] ||= {}
194
+ @where_filter_cache[input_id][property] ||= {}
195
+
196
+ # stash or retrive results to return
197
+ @where_filter_cache[input_id][property][value] ||= begin
198
+ input.select do |object|
199
+ compare_property_vs_target(item_property(object, property), value)
200
+ end.to_a
201
+ end
202
+ end
203
+
204
+ # Filters an array of objects against an expression
205
+ #
206
+ # input - the object array
207
+ # variable - the variable to assign each item to in the expression
208
+ # expression - a Liquid comparison expression passed in as a string
209
+ #
210
+ # Returns the filtered array of objects
211
+ def where_exp(input, variable, expression)
212
+ return input unless input.respond_to?(:select)
213
+
214
+ input = input.values if input.is_a?(Hash) # FIXME
215
+
216
+ condition = parse_condition(expression)
217
+ @context.stack do
218
+ input.select do |object|
219
+ @context[variable] = object
220
+ condition.evaluate(@context)
221
+ end
222
+ end || []
223
+ end
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
+
285
+ # Convert the input into integer
286
+ #
287
+ # input - the object string
288
+ #
289
+ # Returns the integer value
290
+ def to_integer(input)
291
+ return 1 if input == true
292
+ return 0 if input == false
293
+
294
+ input.to_i
295
+ end
296
+
297
+ # Sort an array of objects
298
+ #
299
+ # input - the object array
300
+ # property - property within each object to filter by
301
+ # nils ('first' | 'last') - nils appear before or after non-nil values
302
+ #
303
+ # Returns the filtered array of objects
304
+ def sort(input, property = nil, nils = "first")
305
+ raise ArgumentError, "Cannot sort a null object." if input.nil?
306
+
307
+ if property.nil?
308
+ input.sort
309
+ else
310
+ case nils
311
+ when "first"
312
+ order = - 1
313
+ when "last"
314
+ order = + 1
315
+ else
316
+ raise ArgumentError, "Invalid nils order: " \
317
+ "'#{nils}' is not a valid nils order. It must be 'first' or 'last'."
318
+ end
319
+
320
+ sort_input(input, property, order)
321
+ end
322
+ end
323
+
324
+ def pop(array, num = 1)
325
+ return array unless array.is_a?(Array)
326
+
327
+ num = Liquid::Utils.to_integer(num)
328
+ new_ary = array.dup
329
+ new_ary.pop(num)
330
+ new_ary
331
+ end
332
+
333
+ def push(array, input)
334
+ return array unless array.is_a?(Array)
335
+
336
+ new_ary = array.dup
337
+ new_ary.push(input)
338
+ new_ary
339
+ end
340
+
341
+ def shift(array, num = 1)
342
+ return array unless array.is_a?(Array)
343
+
344
+ num = Liquid::Utils.to_integer(num)
345
+ new_ary = array.dup
346
+ new_ary.shift(num)
347
+ new_ary
348
+ end
349
+
350
+ def unshift(array, input)
351
+ return array unless array.is_a?(Array)
352
+
353
+ new_ary = array.dup
354
+ new_ary.unshift(input)
355
+ new_ary
356
+ end
357
+
358
+ def sample(input, num = 1)
359
+ return input unless input.respond_to?(:sample)
360
+
361
+ num = Liquid::Utils.to_integer(num) rescue 1
362
+ if num == 1
363
+ input.sample
364
+ else
365
+ input.sample(num)
366
+ end
367
+ end
368
+
369
+ # Convert an object into its String representation for debugging
370
+ #
371
+ # input - The Object to be converted
372
+ #
373
+ # Returns a String representation of the object.
374
+ def inspect(input)
375
+ xml_escape(input.inspect)
376
+ end
377
+
378
+ private
379
+
380
+ # Sort the input Enumerable by the given property.
381
+ # If the property doesn't exist, return the sort order respective of
382
+ # which item doesn't have the property.
383
+ # We also utilize the Schwartzian transform to make this more efficient.
384
+ def sort_input(input, property, order)
385
+ input.map { |item| [item_property(item, property), item] }
386
+ .sort! do |a_info, b_info|
387
+ a_property = a_info.first
388
+ b_property = b_info.first
389
+
390
+ if !a_property.nil? && b_property.nil?
391
+ - order
392
+ elsif a_property.nil? && !b_property.nil?
393
+ + order
394
+ else
395
+ a_property <=> b_property || a_property.to_s <=> b_property.to_s
396
+ end
397
+ end
398
+ .map!(&:last)
399
+ end
400
+
401
+ # `where` filter helper
402
+ #
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
+ def item_property(item, property)
425
+ @item_property_cache ||= @context.registers[:site].filter_cache[:item_property] ||= {}
426
+ @item_property_cache[property] ||= {}
427
+ @item_property_cache[property][item] ||= begin
428
+ property = property.to_s
429
+ property = if item.respond_to?(:to_liquid)
430
+ read_liquid_attribute(item.to_liquid, property)
431
+ elsif item.respond_to?(:data)
432
+ item.data[property]
433
+ else
434
+ item[property]
435
+ end
436
+
437
+ parse_sort_input(property)
438
+ end
439
+ end
440
+
441
+ def read_liquid_attribute(liquid_data, property)
442
+ return liquid_data[property] unless property.include?(".")
443
+
444
+ property.split(".").reduce(liquid_data) do |data, key|
445
+ data.respond_to?(:[]) && data[key]
446
+ end
447
+ end
448
+
449
+ FLOAT_LIKE = %r!\A\s*-?(?:\d+\.?\d*|\.\d+)\s*\Z!.freeze
450
+ INTEGER_LIKE = %r!\A\s*-?\d+\s*\Z!.freeze
451
+ private_constant :FLOAT_LIKE, :INTEGER_LIKE
452
+
453
+ # return numeric values as numbers for proper sorting
454
+ def parse_sort_input(property)
455
+ stringified = property.to_s
456
+ return property.to_i if INTEGER_LIKE.match?(stringified)
457
+ return property.to_f if FLOAT_LIKE.match?(stringified)
458
+
459
+ property
460
+ end
461
+
462
+ def as_liquid(item)
463
+ case item
464
+ when Hash
465
+ item.each_with_object({}) { |(k, v), result| result[as_liquid(k)] = as_liquid(v) }
466
+ when Array
467
+ item.map { |i| as_liquid(i) }
468
+ else
469
+ if item.respond_to?(:to_liquid)
470
+ liquidated = item.to_liquid
471
+ # prevent infinite recursion for simple types (which return `self`)
472
+ if liquidated == item
473
+ item
474
+ else
475
+ as_liquid(liquidated)
476
+ end
477
+ else
478
+ item
479
+ end
480
+ end
481
+ end
482
+
483
+ # ----------- The following set of code was *adapted* from Liquid::If
484
+ # ----------- ref: https://git.io/vp6K6
485
+
486
+ # Parse a string to a Liquid Condition
487
+ def parse_condition(exp)
488
+ parser = Liquid::Parser.new(exp)
489
+ condition = parse_binary_comparison(parser)
490
+
491
+ parser.consume(:end_of_string)
492
+ condition
493
+ end
494
+
495
+ # Generate a Liquid::Condition object from a Liquid::Parser object additionally processing
496
+ # the parsed expression based on whether the expression consists of binary operations with
497
+ # Liquid operators `and` or `or`
498
+ #
499
+ # - parser: an instance of Liquid::Parser
500
+ #
501
+ # Returns an instance of Liquid::Condition
502
+ def parse_binary_comparison(parser)
503
+ condition = parse_comparison(parser)
504
+ first_condition = condition
505
+ while (binary_operator = parser.id?("and") || parser.id?("or"))
506
+ child_condition = parse_comparison(parser)
507
+ condition.send(binary_operator, child_condition)
508
+ condition = child_condition
509
+ end
510
+ first_condition
511
+ end
512
+
513
+ # Generates a Liquid::Condition object from a Liquid::Parser object based on whether the parsed
514
+ # expression involves a "comparison" operator (e.g. <, ==, >, !=, etc)
515
+ #
516
+ # - parser: an instance of Liquid::Parser
517
+ #
518
+ # Returns an instance of Liquid::Condition
519
+ def parse_comparison(parser)
520
+ left_operand = Liquid::Expression.parse(parser.expression)
521
+ operator = parser.consume?(:comparison)
522
+
523
+ # No comparison-operator detected. Initialize a Liquid::Condition using only left operand
524
+ return Liquid::Condition.new(left_operand) unless operator
525
+
526
+ # Parse what remained after extracting the left operand and the `:comparison` operator
527
+ # and initialize a Liquid::Condition object using the operands and the comparison-operator
528
+ Liquid::Condition.new(left_operand, operator, Liquid::Expression.parse(parser.expression))
529
+ end
530
+ end
531
+ end
532
+
533
+ Liquid::Template.register_filter(
534
+ Jekyll::Filters
535
+ )