jekyll 4.2.1 → 4.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +350 -350
- data/LICENSE +21 -21
- data/README.markdown +86 -86
- data/exe/jekyll +57 -57
- data/lib/blank_template/_config.yml +3 -3
- data/lib/blank_template/_layouts/default.html +12 -12
- data/lib/blank_template/_sass/main.scss +9 -9
- data/lib/blank_template/assets/css/main.scss +4 -4
- data/lib/blank_template/index.md +8 -8
- data/lib/jekyll/cache.rb +190 -190
- data/lib/jekyll/cleaner.rb +111 -111
- data/lib/jekyll/collection.rb +309 -309
- data/lib/jekyll/command.rb +105 -105
- data/lib/jekyll/commands/build.rb +93 -93
- data/lib/jekyll/commands/clean.rb +45 -45
- data/lib/jekyll/commands/doctor.rb +177 -177
- data/lib/jekyll/commands/help.rb +34 -34
- data/lib/jekyll/commands/new.rb +172 -169
- data/lib/jekyll/commands/new_theme.rb +40 -40
- data/lib/jekyll/commands/serve/live_reload_reactor.rb +122 -122
- data/lib/jekyll/commands/serve/livereload_assets/livereload.js +1183 -1183
- data/lib/jekyll/commands/serve/servlet.rb +202 -202
- data/lib/jekyll/commands/serve/websockets.rb +81 -81
- data/lib/jekyll/commands/serve.rb +362 -362
- data/lib/jekyll/configuration.rb +313 -313
- data/lib/jekyll/converter.rb +54 -54
- data/lib/jekyll/converters/identity.rb +41 -41
- data/lib/jekyll/converters/markdown/kramdown_parser.rb +199 -199
- data/lib/jekyll/converters/markdown.rb +113 -113
- data/lib/jekyll/converters/smartypants.rb +70 -70
- data/lib/jekyll/convertible.rb +257 -257
- data/lib/jekyll/deprecator.rb +50 -50
- data/lib/jekyll/document.rb +544 -544
- data/lib/jekyll/drops/collection_drop.rb +20 -20
- data/lib/jekyll/drops/document_drop.rb +70 -70
- data/lib/jekyll/drops/drop.rb +293 -293
- data/lib/jekyll/drops/excerpt_drop.rb +19 -19
- data/lib/jekyll/drops/jekyll_drop.rb +32 -32
- data/lib/jekyll/drops/site_drop.rb +66 -66
- data/lib/jekyll/drops/static_file_drop.rb +14 -14
- data/lib/jekyll/drops/unified_payload_drop.rb +26 -26
- data/lib/jekyll/drops/url_drop.rb +140 -140
- data/lib/jekyll/entry_filter.rb +121 -121
- data/lib/jekyll/errors.rb +20 -20
- data/lib/jekyll/excerpt.rb +201 -201
- data/lib/jekyll/external.rb +79 -79
- data/lib/jekyll/filters/date_filters.rb +110 -110
- data/lib/jekyll/filters/grouping_filters.rb +64 -64
- data/lib/jekyll/filters/url_filters.rb +98 -98
- data/lib/jekyll/filters.rb +535 -535
- data/lib/jekyll/frontmatter_defaults.rb +240 -240
- data/lib/jekyll/generator.rb +5 -5
- data/lib/jekyll/hooks.rb +107 -107
- data/lib/jekyll/inclusion.rb +32 -32
- data/lib/jekyll/layout.rb +67 -67
- data/lib/jekyll/liquid_extensions.rb +22 -22
- data/lib/jekyll/liquid_renderer/file.rb +77 -77
- data/lib/jekyll/liquid_renderer/table.rb +55 -55
- data/lib/jekyll/liquid_renderer.rb +80 -80
- data/lib/jekyll/log_adapter.rb +151 -151
- data/lib/jekyll/mime.types +866 -866
- data/lib/jekyll/page.rb +217 -217
- data/lib/jekyll/page_excerpt.rb +25 -25
- data/lib/jekyll/page_without_a_file.rb +14 -14
- data/lib/jekyll/path_manager.rb +74 -74
- data/lib/jekyll/plugin.rb +92 -92
- data/lib/jekyll/plugin_manager.rb +115 -115
- data/lib/jekyll/profiler.rb +58 -58
- data/lib/jekyll/publisher.rb +23 -23
- data/lib/jekyll/reader.rb +192 -192
- data/lib/jekyll/readers/collection_reader.rb +23 -23
- data/lib/jekyll/readers/data_reader.rb +79 -79
- data/lib/jekyll/readers/layout_reader.rb +62 -62
- data/lib/jekyll/readers/page_reader.rb +25 -25
- data/lib/jekyll/readers/post_reader.rb +85 -85
- data/lib/jekyll/readers/static_file_reader.rb +25 -25
- data/lib/jekyll/readers/theme_assets_reader.rb +52 -52
- data/lib/jekyll/regenerator.rb +195 -195
- data/lib/jekyll/related_posts.rb +52 -52
- data/lib/jekyll/renderer.rb +265 -265
- data/lib/jekyll/site.rb +551 -551
- data/lib/jekyll/static_file.rb +208 -208
- data/lib/jekyll/stevenson.rb +60 -60
- data/lib/jekyll/tags/highlight.rb +110 -110
- data/lib/jekyll/tags/include.rb +275 -275
- data/lib/jekyll/tags/link.rb +42 -42
- data/lib/jekyll/tags/post_url.rb +106 -106
- data/lib/jekyll/theme.rb +86 -86
- data/lib/jekyll/theme_builder.rb +121 -121
- data/lib/jekyll/url.rb +167 -167
- data/lib/jekyll/utils/ansi.rb +57 -57
- data/lib/jekyll/utils/exec.rb +26 -26
- data/lib/jekyll/utils/internet.rb +37 -37
- data/lib/jekyll/utils/platforms.rb +67 -67
- data/lib/jekyll/utils/thread_event.rb +31 -31
- data/lib/jekyll/utils/win_tz.rb +75 -75
- data/lib/jekyll/utils.rb +367 -367
- data/lib/jekyll/version.rb +5 -5
- data/lib/jekyll.rb +195 -195
- data/lib/site_template/.gitignore +5 -5
- data/lib/site_template/404.html +25 -25
- data/lib/site_template/_config.yml +55 -55
- data/lib/site_template/_posts/0000-00-00-welcome-to-jekyll.markdown.erb +29 -29
- data/lib/site_template/about.markdown +18 -18
- data/lib/site_template/index.markdown +6 -6
- data/lib/theme_template/CODE_OF_CONDUCT.md.erb +74 -74
- data/lib/theme_template/Gemfile +4 -4
- data/lib/theme_template/LICENSE.txt.erb +21 -21
- data/lib/theme_template/README.md.erb +52 -52
- data/lib/theme_template/_layouts/default.html +1 -1
- data/lib/theme_template/_layouts/page.html +5 -5
- data/lib/theme_template/_layouts/post.html +5 -5
- data/lib/theme_template/example/_config.yml.erb +1 -1
- data/lib/theme_template/example/_post.md +12 -12
- data/lib/theme_template/example/index.html +14 -14
- data/lib/theme_template/example/style.scss +7 -7
- data/lib/theme_template/gitignore.erb +6 -6
- data/lib/theme_template/theme.gemspec.erb +16 -16
- data/rubocop/jekyll/assert_equal_literal_actual.rb +149 -149
- data/rubocop/jekyll/no_p_allowed.rb +23 -23
- data/rubocop/jekyll/no_puts_allowed.rb +23 -23
- data/rubocop/jekyll.rb +5 -5
- metadata +3 -3
data/lib/jekyll/filters.rb
CHANGED
|
@@ -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 "bar" <baz>"
|
|
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 "bar" <baz>"
|
|
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
|
+
)
|