bridgetown-core 0.7.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +42 -0
  3. data/bridgetown-core.gemspec +46 -0
  4. data/lib/bridgetown-core.rb +202 -0
  5. data/lib/bridgetown-core/cache.rb +190 -0
  6. data/lib/bridgetown-core/cleaner.rb +111 -0
  7. data/lib/bridgetown-core/collection.rb +279 -0
  8. data/lib/bridgetown-core/command.rb +106 -0
  9. data/lib/bridgetown-core/commands/build.rb +96 -0
  10. data/lib/bridgetown-core/commands/clean.rb +43 -0
  11. data/lib/bridgetown-core/commands/console.rb +56 -0
  12. data/lib/bridgetown-core/commands/doctor.rb +172 -0
  13. data/lib/bridgetown-core/commands/help.rb +34 -0
  14. data/lib/bridgetown-core/commands/new.rb +148 -0
  15. data/lib/bridgetown-core/commands/serve.rb +273 -0
  16. data/lib/bridgetown-core/commands/serve/servlet.rb +68 -0
  17. data/lib/bridgetown-core/configuration.rb +323 -0
  18. data/lib/bridgetown-core/converter.rb +54 -0
  19. data/lib/bridgetown-core/converters/identity.rb +39 -0
  20. data/lib/bridgetown-core/converters/markdown.rb +108 -0
  21. data/lib/bridgetown-core/converters/markdown/kramdown_parser.rb +132 -0
  22. data/lib/bridgetown-core/converters/smartypants.rb +69 -0
  23. data/lib/bridgetown-core/convertible.rb +237 -0
  24. data/lib/bridgetown-core/deprecator.rb +50 -0
  25. data/lib/bridgetown-core/document.rb +475 -0
  26. data/lib/bridgetown-core/drops/bridgetown_drop.rb +32 -0
  27. data/lib/bridgetown-core/drops/collection_drop.rb +20 -0
  28. data/lib/bridgetown-core/drops/document_drop.rb +69 -0
  29. data/lib/bridgetown-core/drops/drop.rb +215 -0
  30. data/lib/bridgetown-core/drops/excerpt_drop.rb +19 -0
  31. data/lib/bridgetown-core/drops/page_drop.rb +14 -0
  32. data/lib/bridgetown-core/drops/site_drop.rb +62 -0
  33. data/lib/bridgetown-core/drops/static_file_drop.rb +14 -0
  34. data/lib/bridgetown-core/drops/unified_payload_drop.rb +26 -0
  35. data/lib/bridgetown-core/drops/url_drop.rb +132 -0
  36. data/lib/bridgetown-core/entry_filter.rb +108 -0
  37. data/lib/bridgetown-core/errors.rb +20 -0
  38. data/lib/bridgetown-core/excerpt.rb +202 -0
  39. data/lib/bridgetown-core/external.rb +62 -0
  40. data/lib/bridgetown-core/filters.rb +467 -0
  41. data/lib/bridgetown-core/filters/date_filters.rb +110 -0
  42. data/lib/bridgetown-core/filters/grouping_filters.rb +64 -0
  43. data/lib/bridgetown-core/filters/url_filters.rb +79 -0
  44. data/lib/bridgetown-core/frontmatter_defaults.rb +238 -0
  45. data/lib/bridgetown-core/generator.rb +5 -0
  46. data/lib/bridgetown-core/hooks.rb +103 -0
  47. data/lib/bridgetown-core/layout.rb +57 -0
  48. data/lib/bridgetown-core/liquid_extensions.rb +22 -0
  49. data/lib/bridgetown-core/liquid_renderer.rb +71 -0
  50. data/lib/bridgetown-core/liquid_renderer/file.rb +67 -0
  51. data/lib/bridgetown-core/liquid_renderer/table.rb +75 -0
  52. data/lib/bridgetown-core/log_adapter.rb +151 -0
  53. data/lib/bridgetown-core/log_writer.rb +60 -0
  54. data/lib/bridgetown-core/mime.types +867 -0
  55. data/lib/bridgetown-core/page.rb +214 -0
  56. data/lib/bridgetown-core/page_without_a_file.rb +14 -0
  57. data/lib/bridgetown-core/path_manager.rb +31 -0
  58. data/lib/bridgetown-core/plugin.rb +80 -0
  59. data/lib/bridgetown-core/plugin_manager.rb +60 -0
  60. data/lib/bridgetown-core/publisher.rb +23 -0
  61. data/lib/bridgetown-core/reader.rb +185 -0
  62. data/lib/bridgetown-core/readers/collection_reader.rb +22 -0
  63. data/lib/bridgetown-core/readers/data_reader.rb +75 -0
  64. data/lib/bridgetown-core/readers/layout_reader.rb +48 -0
  65. data/lib/bridgetown-core/readers/page_reader.rb +24 -0
  66. data/lib/bridgetown-core/readers/post_reader.rb +74 -0
  67. data/lib/bridgetown-core/readers/static_file_reader.rb +24 -0
  68. data/lib/bridgetown-core/regenerator.rb +195 -0
  69. data/lib/bridgetown-core/related_posts.rb +52 -0
  70. data/lib/bridgetown-core/renderer.rb +261 -0
  71. data/lib/bridgetown-core/site.rb +469 -0
  72. data/lib/bridgetown-core/static_file.rb +205 -0
  73. data/lib/bridgetown-core/tags/component.rb +34 -0
  74. data/lib/bridgetown-core/tags/highlight.rb +111 -0
  75. data/lib/bridgetown-core/tags/include.rb +220 -0
  76. data/lib/bridgetown-core/tags/link.rb +41 -0
  77. data/lib/bridgetown-core/tags/post_url.rb +107 -0
  78. data/lib/bridgetown-core/url.rb +164 -0
  79. data/lib/bridgetown-core/utils.rb +367 -0
  80. data/lib/bridgetown-core/utils/ansi.rb +57 -0
  81. data/lib/bridgetown-core/utils/exec.rb +26 -0
  82. data/lib/bridgetown-core/utils/internet.rb +37 -0
  83. data/lib/bridgetown-core/utils/platforms.rb +80 -0
  84. data/lib/bridgetown-core/utils/thread_event.rb +31 -0
  85. data/lib/bridgetown-core/utils/win_tz.rb +75 -0
  86. data/lib/bridgetown-core/version.rb +5 -0
  87. data/lib/bridgetown-core/watcher.rb +139 -0
  88. data/lib/site_template/.gitignore +6 -0
  89. data/lib/site_template/bridgetown.config.yml +21 -0
  90. data/lib/site_template/frontend/javascript/index.js +3 -0
  91. data/lib/site_template/frontend/styles/index.scss +17 -0
  92. data/lib/site_template/package.json +23 -0
  93. data/lib/site_template/src/404.html +9 -0
  94. data/lib/site_template/src/_data/site_metadata.yml +11 -0
  95. data/lib/site_template/src/_includes/footer.html +3 -0
  96. data/lib/site_template/src/_includes/head.html +9 -0
  97. data/lib/site_template/src/_includes/navbar.html +4 -0
  98. data/lib/site_template/src/_layouts/default.html +15 -0
  99. data/lib/site_template/src/_layouts/home.html +7 -0
  100. data/lib/site_template/src/_layouts/page.html +7 -0
  101. data/lib/site_template/src/_layouts/post.html +7 -0
  102. data/lib/site_template/src/_posts/0000-00-00-welcome-to-bridgetown.md.erb +26 -0
  103. data/lib/site_template/src/about.md +11 -0
  104. data/lib/site_template/src/index.md +7 -0
  105. data/lib/site_template/webpack.config.js +60 -0
  106. data/rake/release.rake +30 -0
  107. metadata +106 -1
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module External
5
+ class << self
6
+ #
7
+ # Require a gem or file if it's present, otherwise silently fail.
8
+ #
9
+ # names - a string gem name or array of gem names
10
+ #
11
+ def require_if_present(names)
12
+ Array(names).each do |name|
13
+ begin
14
+ require name
15
+ rescue LoadError
16
+ Bridgetown.logger.debug "Couldn't load #{name}. Skipping."
17
+ yield(name, version_constraint(name)) if block_given?
18
+ false
19
+ end
20
+ end
21
+ end
22
+
23
+ #
24
+ # The version constraint required to activate a given gem.
25
+ #
26
+ # Returns a String version constraint in a parseable form for
27
+ # RubyGems.
28
+ def version_constraint
29
+ "> 0"
30
+ end
31
+
32
+ #
33
+ # Require a gem or gems. If it's not present, show a very nice error
34
+ # message that explains everything and is much more helpful than the
35
+ # normal LoadError.
36
+ #
37
+ # names - a string gem name or array of gem names
38
+ #
39
+ def require_with_graceful_fail(names)
40
+ Array(names).each do |name|
41
+ begin
42
+ Bridgetown.logger.debug "Requiring:", name.to_s
43
+ require name
44
+ rescue LoadError => e
45
+ Bridgetown.logger.error "Dependency Error:", <<~MSG
46
+ Yikes! It looks like you don't have #{name} or one of its dependencies installed.
47
+ In order to use Bridgetown as currently configured, you'll need to install this gem.
48
+
49
+ If you've run Bridgetown with `bundle exec`, ensure that you have included the #{name}
50
+ gem in your Gemfile as well.
51
+
52
+ The full error message from Ruby is: '#{e.message}'
53
+
54
+ If you run into trouble, you can find helpful resources at https://bridgetownrb.com/help/!
55
+ MSG
56
+ raise Bridgetown::Errors::MissingDependencyException, name
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,467 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_all "bridgetown-core/filters"
4
+
5
+ module Bridgetown
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
+ Bridgetown::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
+ Bridgetown::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
+ Bridgetown::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
+ Bridgetown::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
+ property = property.to_s
360
+ property = if item.respond_to?(:to_liquid)
361
+ read_liquid_attribute(item.to_liquid, property)
362
+ elsif item.respond_to?(:data)
363
+ item.data[property]
364
+ else
365
+ item[property]
366
+ end
367
+
368
+ parse_sort_input(property)
369
+ end
370
+ end
371
+
372
+ def read_liquid_attribute(liquid_data, property)
373
+ return liquid_data[property] unless property.include?(".")
374
+
375
+ property.split(".").reduce(liquid_data) do |data, key|
376
+ data.respond_to?(:[]) && data[key]
377
+ end
378
+ end
379
+
380
+ FLOAT_LIKE = %r!\A\s*-?(?:\d+\.?\d*|\.\d+)\s*\Z!.freeze
381
+ INTEGER_LIKE = %r!\A\s*-?\d+\s*\Z!.freeze
382
+ private_constant :FLOAT_LIKE, :INTEGER_LIKE
383
+
384
+ # return numeric values as numbers for proper sorting
385
+ def parse_sort_input(property)
386
+ stringified = property.to_s
387
+ return property.to_i if INTEGER_LIKE.match?(stringified)
388
+ return property.to_f if FLOAT_LIKE.match?(stringified)
389
+
390
+ property
391
+ end
392
+
393
+ def as_liquid(item)
394
+ case item
395
+ when Hash
396
+ pairs = item.map { |k, v| as_liquid([k, v]) }
397
+ Hash[pairs]
398
+ when Array
399
+ item.map { |i| as_liquid(i) }
400
+ else
401
+ if item.respond_to?(:to_liquid)
402
+ liquidated = item.to_liquid
403
+ # prevent infinite recursion for simple types (which return `self`)
404
+ if liquidated == item
405
+ item
406
+ else
407
+ as_liquid(liquidated)
408
+ end
409
+ else
410
+ item
411
+ end
412
+ end
413
+ end
414
+
415
+ # ----------- The following set of code was *adapted* from Liquid::If
416
+ # ----------- ref: https://git.io/vp6K6
417
+
418
+ # Parse a string to a Liquid Condition
419
+ def parse_condition(exp)
420
+ parser = Liquid::Parser.new(exp)
421
+ condition = parse_binary_comparison(parser)
422
+
423
+ parser.consume(:end_of_string)
424
+ condition
425
+ end
426
+
427
+ # Generate a Liquid::Condition object from a Liquid::Parser object additionally processing
428
+ # the parsed expression based on whether the expression consists of binary operations with
429
+ # Liquid operators `and` or `or`
430
+ #
431
+ # - parser: an instance of Liquid::Parser
432
+ #
433
+ # Returns an instance of Liquid::Condition
434
+ def parse_binary_comparison(parser)
435
+ condition = parse_comparison(parser)
436
+ first_condition = condition
437
+ while (binary_operator = parser.id?("and") || parser.id?("or"))
438
+ child_condition = parse_comparison(parser)
439
+ condition.send(binary_operator, child_condition)
440
+ condition = child_condition
441
+ end
442
+ first_condition
443
+ end
444
+
445
+ # Generates a Liquid::Condition object from a Liquid::Parser object based on whether the parsed
446
+ # expression involves a "comparison" operator (e.g. <, ==, >, !=, etc)
447
+ #
448
+ # - parser: an instance of Liquid::Parser
449
+ #
450
+ # Returns an instance of Liquid::Condition
451
+ def parse_comparison(parser)
452
+ left_operand = Liquid::Expression.parse(parser.expression)
453
+ operator = parser.consume?(:comparison)
454
+
455
+ # No comparison-operator detected. Initialize a Liquid::Condition using only left operand
456
+ return Liquid::Condition.new(left_operand) unless operator
457
+
458
+ # Parse what remained after extracting the left operand and the `:comparison` operator
459
+ # and initialize a Liquid::Condition object using the operands and the comparison-operator
460
+ Liquid::Condition.new(left_operand, operator, Liquid::Expression.parse(parser.expression))
461
+ end
462
+ end
463
+ end
464
+
465
+ Liquid::Template.register_filter(
466
+ Bridgetown::Filters
467
+ )