jekyll 4.2.1 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,535 +1,532 @@
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 retrieve results to return
197
+ @where_filter_cache[input_id][property][value] ||= input.select do |object|
198
+ compare_property_vs_target(item_property(object, property), value)
199
+ end.to_a
200
+ end
201
+
202
+ # Filters an array of objects against an expression
203
+ #
204
+ # input - the object array
205
+ # variable - the variable to assign each item to in the expression
206
+ # expression - a Liquid comparison expression passed in as a string
207
+ #
208
+ # Returns the filtered array of objects
209
+ def where_exp(input, variable, expression)
210
+ return input unless input.respond_to?(:select)
211
+
212
+ input = input.values if input.is_a?(Hash) # FIXME
213
+
214
+ condition = parse_condition(expression)
215
+ @context.stack do
216
+ input.select do |object|
217
+ @context[variable] = object
218
+ condition.evaluate(@context)
219
+ end
220
+ end || []
221
+ end
222
+
223
+ # Search an array of objects and returns the first object that has the queried attribute
224
+ # with the given value or returns nil otherwise.
225
+ #
226
+ # input - the object array.
227
+ # property - the property within each object to search by.
228
+ # value - the desired value.
229
+ # Cannot be an instance of Array nor Hash since calling #to_s on them returns
230
+ # their `#inspect` string object.
231
+ #
232
+ # Returns the found object or nil
233
+ #
234
+ # rubocop:disable Metrics/CyclomaticComplexity
235
+ def find(input, property, value)
236
+ return input if !property || value.is_a?(Array) || value.is_a?(Hash)
237
+ return input unless input.respond_to?(:find)
238
+
239
+ input = input.values if input.is_a?(Hash)
240
+ input_id = input.hash
241
+
242
+ # implement a hash based on method parameters to cache the end-result for given parameters.
243
+ @find_filter_cache ||= {}
244
+ @find_filter_cache[input_id] ||= {}
245
+ @find_filter_cache[input_id][property] ||= {}
246
+
247
+ # stash or retrieve results to return
248
+ # Since `enum.find` can return nil or false, we use a placeholder string "<__NO MATCH__>"
249
+ # to validate caching.
250
+ result = @find_filter_cache[input_id][property][value] ||= input.find do |object|
251
+ compare_property_vs_target(item_property(object, property), value)
252
+ end || "<__NO MATCH__>"
253
+
254
+ return nil if result == "<__NO MATCH__>"
255
+
256
+ result
257
+ end
258
+ # rubocop:enable Metrics/CyclomaticComplexity
259
+
260
+ # Searches an array of objects against an expression and returns the first object for which
261
+ # the expression evaluates to true, or returns nil otherwise.
262
+ #
263
+ # input - the object array
264
+ # variable - the variable to assign each item to in the expression
265
+ # expression - a Liquid comparison expression passed in as a string
266
+ #
267
+ # Returns the found object or nil
268
+ def find_exp(input, variable, expression)
269
+ return input unless input.respond_to?(:find)
270
+
271
+ input = input.values if input.is_a?(Hash)
272
+
273
+ condition = parse_condition(expression)
274
+ @context.stack do
275
+ input.find do |object|
276
+ @context[variable] = object
277
+ condition.evaluate(@context)
278
+ end
279
+ end
280
+ end
281
+
282
+ # Convert the input into integer
283
+ #
284
+ # input - the object string
285
+ #
286
+ # Returns the integer value
287
+ def to_integer(input)
288
+ return 1 if input == true
289
+ return 0 if input == false
290
+
291
+ input.to_i
292
+ end
293
+
294
+ # Sort an array of objects
295
+ #
296
+ # input - the object array
297
+ # property - property within each object to filter by
298
+ # nils ('first' | 'last') - nils appear before or after non-nil values
299
+ #
300
+ # Returns the filtered array of objects
301
+ def sort(input, property = nil, nils = "first")
302
+ raise ArgumentError, "Cannot sort a null object." if input.nil?
303
+
304
+ if property.nil?
305
+ input.sort
306
+ else
307
+ case nils
308
+ when "first"
309
+ order = - 1
310
+ when "last"
311
+ order = + 1
312
+ else
313
+ raise ArgumentError, "Invalid nils order: " \
314
+ "'#{nils}' is not a valid nils order. It must be 'first' or 'last'."
315
+ end
316
+
317
+ sort_input(input, property, order)
318
+ end
319
+ end
320
+
321
+ def pop(array, num = 1)
322
+ return array unless array.is_a?(Array)
323
+
324
+ num = Liquid::Utils.to_integer(num)
325
+ new_ary = array.dup
326
+ new_ary.pop(num)
327
+ new_ary
328
+ end
329
+
330
+ def push(array, input)
331
+ return array unless array.is_a?(Array)
332
+
333
+ new_ary = array.dup
334
+ new_ary.push(input)
335
+ new_ary
336
+ end
337
+
338
+ def shift(array, num = 1)
339
+ return array unless array.is_a?(Array)
340
+
341
+ num = Liquid::Utils.to_integer(num)
342
+ new_ary = array.dup
343
+ new_ary.shift(num)
344
+ new_ary
345
+ end
346
+
347
+ def unshift(array, input)
348
+ return array unless array.is_a?(Array)
349
+
350
+ new_ary = array.dup
351
+ new_ary.unshift(input)
352
+ new_ary
353
+ end
354
+
355
+ def sample(input, num = 1)
356
+ return input unless input.respond_to?(:sample)
357
+
358
+ num = Liquid::Utils.to_integer(num) rescue 1
359
+ if num == 1
360
+ input.sample
361
+ else
362
+ input.sample(num)
363
+ end
364
+ end
365
+
366
+ # Convert an object into its String representation for debugging
367
+ #
368
+ # input - The Object to be converted
369
+ #
370
+ # Returns a String representation of the object.
371
+ def inspect(input)
372
+ xml_escape(input.inspect)
373
+ end
374
+
375
+ private
376
+
377
+ # Sort the input Enumerable by the given property.
378
+ # If the property doesn't exist, return the sort order respective of
379
+ # which item doesn't have the property.
380
+ # We also utilize the Schwartzian transform to make this more efficient.
381
+ def sort_input(input, property, order)
382
+ input.map { |item| [item_property(item, property), item] }
383
+ .sort! do |a_info, b_info|
384
+ a_property = a_info.first
385
+ b_property = b_info.first
386
+
387
+ if !a_property.nil? && b_property.nil?
388
+ - order
389
+ elsif a_property.nil? && !b_property.nil?
390
+ + order
391
+ else
392
+ a_property <=> b_property || a_property.to_s <=> b_property.to_s
393
+ end
394
+ end
395
+ .map!(&:last)
396
+ end
397
+
398
+ # `where` filter helper
399
+ #
400
+ def compare_property_vs_target(property, target)
401
+ case target
402
+ when NilClass
403
+ return true if property.nil?
404
+ when Liquid::Expression::MethodLiteral # `empty` or `blank`
405
+ target = target.to_s
406
+ return true if property == target || Array(property).join == target
407
+ else
408
+ target = target.to_s
409
+ if property.is_a? String
410
+ return true if property == target
411
+ else
412
+ Array(property).each do |prop|
413
+ return true if prop.to_s == target
414
+ end
415
+ end
416
+ end
417
+
418
+ false
419
+ end
420
+
421
+ def item_property(item, property)
422
+ @item_property_cache ||= @context.registers[:site].filter_cache[:item_property] ||= {}
423
+ @item_property_cache[property] ||= {}
424
+ @item_property_cache[property][item] ||= begin
425
+ property = property.to_s
426
+ property = if item.respond_to?(:to_liquid)
427
+ read_liquid_attribute(item.to_liquid, property)
428
+ elsif item.respond_to?(:data)
429
+ item.data[property]
430
+ else
431
+ item[property]
432
+ end
433
+
434
+ parse_sort_input(property)
435
+ end
436
+ end
437
+
438
+ def read_liquid_attribute(liquid_data, property)
439
+ return liquid_data[property] unless property.include?(".")
440
+
441
+ property.split(".").reduce(liquid_data) do |data, key|
442
+ data.respond_to?(:[]) && data[key]
443
+ end
444
+ end
445
+
446
+ FLOAT_LIKE = %r!\A\s*-?(?:\d+\.?\d*|\.\d+)\s*\Z!.freeze
447
+ INTEGER_LIKE = %r!\A\s*-?\d+\s*\Z!.freeze
448
+ private_constant :FLOAT_LIKE, :INTEGER_LIKE
449
+
450
+ # return numeric values as numbers for proper sorting
451
+ def parse_sort_input(property)
452
+ stringified = property.to_s
453
+ return property.to_i if INTEGER_LIKE.match?(stringified)
454
+ return property.to_f if FLOAT_LIKE.match?(stringified)
455
+
456
+ property
457
+ end
458
+
459
+ def as_liquid(item)
460
+ case item
461
+ when Hash
462
+ item.each_with_object({}) { |(k, v), result| result[as_liquid(k)] = as_liquid(v) }
463
+ when Array
464
+ item.map { |i| as_liquid(i) }
465
+ else
466
+ if item.respond_to?(:to_liquid)
467
+ liquidated = item.to_liquid
468
+ # prevent infinite recursion for simple types (which return `self`)
469
+ if liquidated == item
470
+ item
471
+ else
472
+ as_liquid(liquidated)
473
+ end
474
+ else
475
+ item
476
+ end
477
+ end
478
+ end
479
+
480
+ # ----------- The following set of code was *adapted* from Liquid::If
481
+ # ----------- ref: https://github.com/Shopify/liquid/blob/ffb0ace30315bbcf3548a0383fab531452060ae8/lib/liquid/tags/if.rb#L84-L107
482
+
483
+ # Parse a string to a Liquid Condition
484
+ def parse_condition(exp)
485
+ parser = Liquid::Parser.new(exp)
486
+ condition = parse_binary_comparison(parser)
487
+
488
+ parser.consume(:end_of_string)
489
+ condition
490
+ end
491
+
492
+ # Generate a Liquid::Condition object from a Liquid::Parser object additionally processing
493
+ # the parsed expression based on whether the expression consists of binary operations with
494
+ # Liquid operators `and` or `or`
495
+ #
496
+ # - parser: an instance of Liquid::Parser
497
+ #
498
+ # Returns an instance of Liquid::Condition
499
+ def parse_binary_comparison(parser)
500
+ condition = parse_comparison(parser)
501
+ first_condition = condition
502
+ while (binary_operator = parser.id?("and") || parser.id?("or"))
503
+ child_condition = parse_comparison(parser)
504
+ condition.send(binary_operator, child_condition)
505
+ condition = child_condition
506
+ end
507
+ first_condition
508
+ end
509
+
510
+ # Generates a Liquid::Condition object from a Liquid::Parser object based on whether the parsed
511
+ # expression involves a "comparison" operator (e.g. <, ==, >, !=, etc)
512
+ #
513
+ # - parser: an instance of Liquid::Parser
514
+ #
515
+ # Returns an instance of Liquid::Condition
516
+ def parse_comparison(parser)
517
+ left_operand = Liquid::Expression.parse(parser.expression)
518
+ operator = parser.consume?(:comparison)
519
+
520
+ # No comparison-operator detected. Initialize a Liquid::Condition using only left operand
521
+ return Liquid::Condition.new(left_operand) unless operator
522
+
523
+ # Parse what remained after extracting the left operand and the `:comparison` operator
524
+ # and initialize a Liquid::Condition object using the operands and the comparison-operator
525
+ Liquid::Condition.new(left_operand, operator, Liquid::Expression.parse(parser.expression))
526
+ end
527
+ end
528
+ end
529
+
530
+ Liquid::Template.register_filter(
531
+ Jekyll::Filters
532
+ )