jekyll 4.0.1 → 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 -163
  3. data/LICENSE +21 -21
  4. data/README.markdown +86 -90
  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 -103
  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 -173
  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 -42
  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 -354
  26. data/lib/jekyll/configuration.rb +313 -316
  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 -130
  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 -254
  33. data/lib/jekyll/deprecator.rb +50 -50
  34. data/lib/jekyll/document.rb +544 -522
  35. data/lib/jekyll/drops/collection_drop.rb +20 -20
  36. data/lib/jekyll/drops/document_drop.rb +70 -69
  37. data/lib/jekyll/drops/drop.rb +293 -215
  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 -132
  44. data/lib/jekyll/entry_filter.rb +121 -110
  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 -68
  51. data/lib/jekyll/filters.rb +535 -454
  52. data/lib/jekyll/frontmatter_defaults.rb +240 -245
  53. data/lib/jekyll/generator.rb +5 -5
  54. data/lib/jekyll/hooks.rb +107 -106
  55. data/lib/jekyll/inclusion.rb +32 -0
  56. data/lib/jekyll/layout.rb +67 -62
  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 -75
  60. data/lib/jekyll/liquid_renderer.rb +80 -77
  61. data/lib/jekyll/log_adapter.rb +151 -151
  62. data/lib/jekyll/mime.types +866 -866
  63. data/lib/jekyll/page.rb +217 -186
  64. data/lib/jekyll/page_excerpt.rb +25 -0
  65. data/lib/jekyll/page_without_a_file.rb +14 -14
  66. data/lib/jekyll/path_manager.rb +74 -31
  67. data/lib/jekyll/plugin.rb +92 -92
  68. data/lib/jekyll/plugin_manager.rb +115 -115
  69. data/lib/jekyll/profiler.rb +58 -0
  70. data/lib/jekyll/publisher.rb +23 -23
  71. data/lib/jekyll/reader.rb +192 -187
  72. data/lib/jekyll/readers/collection_reader.rb +23 -22
  73. data/lib/jekyll/readers/data_reader.rb +79 -75
  74. data/lib/jekyll/readers/layout_reader.rb +62 -61
  75. data/lib/jekyll/readers/page_reader.rb +25 -24
  76. data/lib/jekyll/readers/post_reader.rb +85 -84
  77. data/lib/jekyll/readers/static_file_reader.rb +25 -24
  78. data/lib/jekyll/readers/theme_assets_reader.rb +52 -51
  79. data/lib/jekyll/regenerator.rb +195 -195
  80. data/lib/jekyll/related_posts.rb +52 -52
  81. data/lib/jekyll/renderer.rb +265 -267
  82. data/lib/jekyll/site.rb +551 -527
  83. data/lib/jekyll/static_file.rb +208 -203
  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 -221
  87. data/lib/jekyll/tags/link.rb +42 -41
  88. data/lib/jekyll/tags/post_url.rb +106 -107
  89. data/lib/jekyll/theme.rb +86 -80
  90. data/lib/jekyll/theme_builder.rb +121 -121
  91. data/lib/jekyll/url.rb +167 -164
  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 -82
  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 -206
  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 -19
  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 +20 -38
@@ -1,454 +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+!, " ").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)
125
- input.split.length
126
- end
127
-
128
- # Join an array of things into a string by separating with commas and the
129
- # word "and" for the last one.
130
- #
131
- # array - The Array of Strings to join.
132
- # connector - Word used to connect the last 2 items in the array
133
- #
134
- # Examples
135
- #
136
- # array_to_sentence_string(["apples", "oranges", "grapes"])
137
- # # => "apples, oranges, and grapes"
138
- #
139
- # Returns the formatted String.
140
- def array_to_sentence_string(array, connector = "and")
141
- case array.length
142
- when 0
143
- ""
144
- when 1
145
- array[0].to_s
146
- when 2
147
- "#{array[0]} #{connector} #{array[1]}"
148
- else
149
- "#{array[0...-1].join(", ")}, #{connector} #{array[-1]}"
150
- end
151
- end
152
-
153
- # Convert the input into json string
154
- #
155
- # input - The Array or Hash to be converted
156
- #
157
- # Returns the converted json string
158
- def jsonify(input)
159
- as_liquid(input).to_json
160
- end
161
-
162
- # Filter an array of objects
163
- #
164
- # input - the object array.
165
- # property - the property within each object to filter by.
166
- # value - the desired value.
167
- # Cannot be an instance of Array nor Hash since calling #to_s on them returns
168
- # their `#inspect` string object.
169
- #
170
- # Returns the filtered array of objects
171
- def where(input, property, value)
172
- return input if !property || value.is_a?(Array) || value.is_a?(Hash)
173
- return input unless input.respond_to?(:select)
174
-
175
- input = input.values if input.is_a?(Hash)
176
- input_id = input.hash
177
-
178
- # implement a hash based on method parameters to cache the end-result
179
- # for given parameters.
180
- @where_filter_cache ||= {}
181
- @where_filter_cache[input_id] ||= {}
182
- @where_filter_cache[input_id][property] ||= {}
183
-
184
- # stash or retrive results to return
185
- @where_filter_cache[input_id][property][value] ||= begin
186
- input.select do |object|
187
- compare_property_vs_target(item_property(object, property), value)
188
- end.to_a
189
- end
190
- end
191
-
192
- # Filters an array of objects against an expression
193
- #
194
- # input - the object array
195
- # variable - the variable to assign each item to in the expression
196
- # expression - a Liquid comparison expression passed in as a string
197
- #
198
- # Returns the filtered array of objects
199
- def where_exp(input, variable, expression)
200
- return input unless input.respond_to?(:select)
201
-
202
- input = input.values if input.is_a?(Hash) # FIXME
203
-
204
- condition = parse_condition(expression)
205
- @context.stack do
206
- input.select do |object|
207
- @context[variable] = object
208
- condition.evaluate(@context)
209
- end
210
- end || []
211
- end
212
-
213
- # Convert the input into integer
214
- #
215
- # input - the object string
216
- #
217
- # Returns the integer value
218
- def to_integer(input)
219
- return 1 if input == true
220
- return 0 if input == false
221
-
222
- input.to_i
223
- end
224
-
225
- # Sort an array of objects
226
- #
227
- # input - the object array
228
- # property - property within each object to filter by
229
- # nils ('first' | 'last') - nils appear before or after non-nil values
230
- #
231
- # Returns the filtered array of objects
232
- def sort(input, property = nil, nils = "first")
233
- raise ArgumentError, "Cannot sort a null object." if input.nil?
234
-
235
- if property.nil?
236
- input.sort
237
- else
238
- if nils == "first"
239
- order = - 1
240
- elsif nils == "last"
241
- order = + 1
242
- else
243
- raise ArgumentError, "Invalid nils order: " \
244
- "'#{nils}' is not a valid nils order. It must be 'first' or 'last'."
245
- end
246
-
247
- sort_input(input, property, order)
248
- end
249
- end
250
-
251
- def pop(array, num = 1)
252
- return array unless array.is_a?(Array)
253
-
254
- num = Liquid::Utils.to_integer(num)
255
- new_ary = array.dup
256
- new_ary.pop(num)
257
- new_ary
258
- end
259
-
260
- def push(array, input)
261
- return array unless array.is_a?(Array)
262
-
263
- new_ary = array.dup
264
- new_ary.push(input)
265
- new_ary
266
- end
267
-
268
- def shift(array, num = 1)
269
- return array unless array.is_a?(Array)
270
-
271
- num = Liquid::Utils.to_integer(num)
272
- new_ary = array.dup
273
- new_ary.shift(num)
274
- new_ary
275
- end
276
-
277
- def unshift(array, input)
278
- return array unless array.is_a?(Array)
279
-
280
- new_ary = array.dup
281
- new_ary.unshift(input)
282
- new_ary
283
- end
284
-
285
- def sample(input, num = 1)
286
- return input unless input.respond_to?(:sample)
287
-
288
- num = Liquid::Utils.to_integer(num) rescue 1
289
- if num == 1
290
- input.sample
291
- else
292
- input.sample(num)
293
- end
294
- end
295
-
296
- # Convert an object into its String representation for debugging
297
- #
298
- # input - The Object to be converted
299
- #
300
- # Returns a String representation of the object.
301
- def inspect(input)
302
- xml_escape(input.inspect)
303
- end
304
-
305
- private
306
-
307
- # Sort the input Enumerable by the given property.
308
- # If the property doesn't exist, return the sort order respective of
309
- # which item doesn't have the property.
310
- # We also utilize the Schwartzian transform to make this more efficient.
311
- def sort_input(input, property, order)
312
- input.map { |item| [item_property(item, property), item] }
313
- .sort! do |a_info, b_info|
314
- a_property = a_info.first
315
- b_property = b_info.first
316
-
317
- if !a_property.nil? && b_property.nil?
318
- - order
319
- elsif a_property.nil? && !b_property.nil?
320
- + order
321
- else
322
- a_property <=> b_property || a_property.to_s <=> b_property.to_s
323
- end
324
- end
325
- .map!(&:last)
326
- end
327
-
328
- # `where` filter helper
329
- #
330
- # rubocop:disable Metrics/CyclomaticComplexity
331
- # rubocop:disable Metrics/PerceivedComplexity
332
- def compare_property_vs_target(property, target)
333
- case target
334
- when NilClass
335
- return true if property.nil?
336
- when Liquid::Expression::MethodLiteral # `empty` or `blank`
337
- target = target.to_s
338
- return true if property == target || Array(property).join == target
339
- else
340
- target = target.to_s
341
- if property.is_a? String
342
- return true if property == target
343
- else
344
- Array(property).each do |prop|
345
- return true if prop.to_s == target
346
- end
347
- end
348
- end
349
-
350
- false
351
- end
352
- # rubocop:enable Metrics/CyclomaticComplexity
353
- # rubocop:enable Metrics/PerceivedComplexity
354
-
355
- def item_property(item, property)
356
- @item_property_cache ||= {}
357
- @item_property_cache[property] ||= {}
358
- @item_property_cache[property][item] ||= begin
359
- if item.respond_to?(:to_liquid)
360
- property.to_s.split(".").reduce(item.to_liquid) do |subvalue, attribute|
361
- parse_sort_input(subvalue[attribute])
362
- end
363
- elsif item.respond_to?(:data)
364
- parse_sort_input(item.data[property.to_s])
365
- else
366
- parse_sort_input(item[property.to_s])
367
- end
368
- end
369
- end
370
-
371
- FLOAT_LIKE = %r!\A\s*-?(?:\d+\.?\d*|\.\d+)\s*\Z!.freeze
372
- INTEGER_LIKE = %r!\A\s*-?\d+\s*\Z!.freeze
373
- private_constant :FLOAT_LIKE, :INTEGER_LIKE
374
-
375
- # return numeric values as numbers for proper sorting
376
- def parse_sort_input(property)
377
- stringified = property.to_s
378
- return property.to_i if INTEGER_LIKE.match?(stringified)
379
- return property.to_f if FLOAT_LIKE.match?(stringified)
380
-
381
- property
382
- end
383
-
384
- def as_liquid(item)
385
- case item
386
- when Hash
387
- pairs = item.map { |k, v| as_liquid([k, v]) }
388
- Hash[pairs]
389
- when Array
390
- item.map { |i| as_liquid(i) }
391
- else
392
- if item.respond_to?(:to_liquid)
393
- liquidated = item.to_liquid
394
- # prevent infinite recursion for simple types (which return `self`)
395
- if liquidated == item
396
- item
397
- else
398
- as_liquid(liquidated)
399
- end
400
- else
401
- item
402
- end
403
- end
404
- end
405
-
406
- # ----------- The following set of code was *adapted* from Liquid::If
407
- # ----------- ref: https://git.io/vp6K6
408
-
409
- # Parse a string to a Liquid Condition
410
- def parse_condition(exp)
411
- parser = Liquid::Parser.new(exp)
412
- condition = parse_binary_comparison(parser)
413
-
414
- parser.consume(:end_of_string)
415
- condition
416
- end
417
-
418
- # Generate a Liquid::Condition object from a Liquid::Parser object additionally processing
419
- # the parsed expression based on whether the expression consists of binary operations with
420
- # Liquid operators `and` or `or`
421
- #
422
- # - parser: an instance of Liquid::Parser
423
- #
424
- # Returns an instance of Liquid::Condition
425
- def parse_binary_comparison(parser)
426
- parse_comparison(parser).tap do |condition|
427
- binary_operator = parser.id?("and") || parser.id?("or")
428
- condition.send(binary_operator, parse_comparison(parser)) if binary_operator
429
- end
430
- end
431
-
432
- # Generates a Liquid::Condition object from a Liquid::Parser object based on whether the parsed
433
- # expression involves a "comparison" operator (e.g. <, ==, >, !=, etc)
434
- #
435
- # - parser: an instance of Liquid::Parser
436
- #
437
- # Returns an instance of Liquid::Condition
438
- def parse_comparison(parser)
439
- left_operand = Liquid::Expression.parse(parser.expression)
440
- operator = parser.consume?(:comparison)
441
-
442
- # No comparison-operator detected. Initialize a Liquid::Condition using only left operand
443
- return Liquid::Condition.new(left_operand) unless operator
444
-
445
- # Parse what remained after extracting the left operand and the `:comparison` operator
446
- # and initialize a Liquid::Condition object using the operands and the comparison-operator
447
- Liquid::Condition.new(left_operand, operator, Liquid::Expression.parse(parser.expression))
448
- end
449
- end
450
- end
451
-
452
- Liquid::Template.register_filter(
453
- Jekyll::Filters
454
- )
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
+ )