jekyll 4.2.0 → 4.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +350 -347
  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 +169 -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 -365
  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 -260
  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 -270
  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
+ )