Compare commits

...
Author SHA1 Message Date
Guilherme Carreiro f9454d8cf3 Fix array filters to not support nested properties 2025-01-31 13:53:17 +01:00
Guilherme CarreiroandGuilherme Carreiro 8dd9279265 Fix release date on History.md 2025-01-24 15:34:48 +01:00
Guilherme CarreiroandGuilherme Carreiro bf1419b8ac Apply the same fix for find_index and has 2025-01-24 08:39:01 +01:00
Guilherme CarreiroandGuilherme Carreiro 5718c4cee2 Fix the find filter to return nil when filtering empty arrays 2025-01-24 08:39:01 +01:00
Guilherme CarreiroandGitHub b0dbc62696 Fix bundle exec rake example (#1900)
* Fix `bundle exec rake example`

* Move 'webrick' from 'benchmark/test' to 'development'
2025-01-23 08:21:38 +01:00
03aafa974c Bump to 5.7.0 (#1894)
* Bump to 5.7.0

* Update 'History.md'

* Update 'Releasing' steps

* Update CONTRIBUTING.md

Co-authored-by: Gray Gilmore <[email protected]>

* Update History.md

Co-authored-by: Ian Ker-Seymer <[email protected]>

---------

Co-authored-by: Gray Gilmore <[email protected]>
Co-authored-by: Ian Ker-Seymer <[email protected]>
2025-01-17 12:10:18 +01:00
Ian Ker-SeymerandGitHub 6372289ba3 Ensure we use InputIterator#each when in join filter (#1898) 2025-01-16 11:36:36 -05:00
Ian Ker-SeymerandGitHub 0ec52a40b5 Use Liquid::Utils.to_s for join filter (#1897) 2025-01-16 11:21:58 -05:00
Ian Ker-SeymerandGitHub 74af735f0e Allow for custom < Hash classes to override #to_s (#1896) 2025-01-16 11:16:13 -05:00
4b65a28722 Implement logic for stringify Hashes to keep compat with 3.4 (#1892)
* Exploring

* Bump to v5.6.5

---------

Co-authored-by: Dominic Petrick <[email protected]>
2025-01-15 16:36:53 -05:00
ecf25ea83d Add the "Releasing" section to CONTRIBUTING.md (#1891)
* Add 'Releasing' workflow to 'CONTRIBUTING.md'

* Update CONTRIBUTING.md

Co-authored-by: Michael Go <[email protected]>

---------

Co-authored-by: Michael Go <[email protected]>
2025-01-14 19:07:11 +01:00
6909570f8e Add find, find_index, has, and reject filters to arrays (#1869)
* Add reject filter #1573

* Add deep search for filter taking in properties #1749

* Update branch with main

* Add `find`, `find_index`, `has`, and `reject` filters to arrays

* Refactor: avoid usage of public_send

---------

Co-authored-by: Anders Søgaard <[email protected]>
Co-authored-by: Anders Søgaard <[email protected]>
2025-01-14 10:33:31 +01:00
Michael GoandGitHub cd9971579f Merge pull request #1889 from Shopify/default-variable-lookup-string-scanner
Add default string_scanner to `Liquid::VariableLookup.parse`
2025-01-13 23:28:11 -04:00
Ian Ker-Seymer 8e37c5e18b Bump to v5.6.4 2025-01-13 21:57:52 -05:00
Ian Ker-Seymer b3f9639e7d Add default string_scanner to Liquid::VariableLookup.parse 2025-01-13 21:56:39 -05:00
Michael GoandGitHub fe3da0e17d Merge pull request #1887 from Shopify/remove-lru-redux
remove lru-redux
2025-01-13 18:37:17 -04:00
Michael Go e200c4544b remove expression caching while rendering 2025-01-13 18:36:30 -04:00
Michael Go 7124540563 remove lru-redux 2025-01-13 18:27:56 -04:00
Michael GoandGitHub 0558bd12c4 Merge pull request #1886 from Shopify/fix-parsing-float-with-leading-point
float has to start with a digit
2025-01-13 17:17:15 -04:00
Michael Go 0639a094a8 bump version to 5.6.2 2025-01-13 17:16:33 -04:00
Michael Go 58777bcd93 float has to start with a digit 2025-01-13 17:13:27 -04:00
Michael GoandGitHub 323951b36f Merge pull request #1844 from Shopify/fast-expression-parse
Faster Expression parser / Tokenizer with StringScanner
2025-01-10 15:34:06 -04:00
Benjamin SteinandGitHub a5b91e83ae Update README.md with default environment (#1879)
Smaller projects, especially when upgrading from earlier versions, may not need different environment scopes, so just adding to the docs.
2025-01-10 14:28:46 -05:00
16 changed files with 652 additions and 77 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
- { ruby: 3.0, allowed-failure: false } # minimum supported
- { ruby: 3.2, allowed-failure: false }
- { ruby: 3.3, allowed-failure: false }
- { ruby: "3.4.0-rc1", allowed-failure: false } # latest
- { ruby: 3.4, allowed-failure: false } # latest
- { ruby: ruby-head, allowed-failure: false }
name: Test Ruby ${{ matrix.entry.ruby }}
steps:
+1 -1
View File
@@ -1 +1 @@
3.3.6
3.4.1
+8
View File
@@ -26,3 +26,11 @@
* If it makes sense, add tests for your code and/or run a performance benchmark
* Make sure all tests pass (`bundle exec rake`)
* Create a pull request
## Releasing
* Bump the version in `lib/liquid/version.rb`
* Update the `History.md` file
* Open a PR like [this one](https://github.com/Shopify/liquid/pull/1894) and merge it to `main`
* Create a new release using the [GitHub UI](https://github.com/Shopify/liquid/releases/new)
+4
View File
@@ -20,6 +20,10 @@ group :benchmark, :test do
end
end
group :development do
gem "webrick"
end
group :test do
gem 'rubocop', '~> 1.61.0'
gem 'rubocop-shopify', '~> 2.12.0', require: false
+54 -2
View File
@@ -1,12 +1,64 @@
# Liquid Change Log
## 5.6.0 (unreleased)
## 5.8.0 (unreleased)
## 5.7.2 2025-01-31
* Fix array filters to not support nested properties
## 5.7.1 2025-01-24
* Fix the `find` and `find_index`filters to return `nil` when filtering empty arrays
* Fix the `has` filter to return `false` when filtering empty arrays
## 5.7.0 2025-01-16
### Features
* Add `find`, `find_index`, `has`, and `reject` filters to arrays
* Compatibility with Ruby 3.4
## 5.6.4 2025-01-14
### Fixes
* Add a default `string_scanner` to avoid errors with `Liquid::VariableLookup.parse("foo.bar")` [Ian Ker-Seymer]
## 5.6.3 2025-01-13
* Remove `lru_redux` dependency [Michael Go]
## 5.6.2 2025-01-13
### Fixes
* Preserve the old behavior of requiring floats to start with a digit [Michael Go]
## 5.6.1 2025-01-13
### Performance improvements
* Faster Expression parser / Tokenizer with StringScanner [Michael Go]
## 5.6.0 2024-12-19
### Architectural changes
* Added new `Environment` class to manage configuration and state that was previously stored in `Template` [Ian Ker-Seymer]
* Moved tag registration from `Template` to `Environment` [Ian Ker-Seymer]
* Removed `StrainerFactory` in favor of `Environment`-based strainer creation [Ian Ker-Seymer]
* Consolidated standard tags into a new `Tags` module with `STANDARD_TAGS` constant [Ian Ker-Seymer]
### Performance improvements
* Optimized `Lexer` with a new `Lexer2` implementation using jump tables for faster tokenization, requires Ruby 3.4 [Ian Ker-Seymer]
* Improved variable rendering with specialized handling for different types [Michael Go]
* Reduced array allocations by using frozen empty constants [Michael Go]
### API changes
* Deprecated several `Template` class methods in favor of `Environment` methods [Ian Ker-Seymer]
* Added deprecation warnings system [Ian Ker-Seymer]
* Changed how filters and tags are registered to use Environment [Ian Ker-Seymer]
### Fixes
* Fixed table row handling of break interrupts [Alex Coco]
* Improved variable output handling for arrays [Ian Ker-Seymer]
* Fix Tokenizer to handle null source value (#1873) [Bahar Pourazar]
## 5.5.0 2024-03-21
Please reference the GitHub release for more information.
+1 -1
View File
@@ -91,7 +91,7 @@ Liquid::Template.parse(<<~LIQUID, environment: email_environment)
LIQUID
```
By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage.
By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage. For smaller projects, a global environment is available via `Liquid::Environment.default`.
### Error Modes
+1 -4
View File
@@ -1,7 +1,5 @@
# frozen_string_literal: true
require "lru_redux"
module Liquid
# Context keeps the variable stack and resolves variables, as well as keywords
#
@@ -41,7 +39,6 @@ module Liquid
@filters = []
@global_filter = nil
@disabled_tags = {}
@expression_cache = LruRedux::ThreadSafeCache.new(1000)
# Instead of constructing new StringScanner objects for each Expression parse,
# we recycle the same one.
@@ -183,7 +180,7 @@ module Liquid
# Example:
# products == empty #=> products.empty?
def [](expression)
evaluate(Expression.parse(expression, @string_scanner, @expression_cache))
evaluate(Expression.parse(expression, @string_scanner))
end
def key?(key)
+9 -4
View File
@@ -1,7 +1,5 @@
# frozen_string_literal: true
require "lru_redux"
module Liquid
class Expression
LITERALS = {
@@ -79,10 +77,17 @@ module Liquid
end
ss.string = markup
# the first byte must be a digit, a period, or a dash
# the first byte must be a digit or a dash
byte = ss.scan_byte
return false if byte != DASH && byte != DOT && (byte < ZERO || byte > NINE)
return false if byte != DASH && (byte < ZERO || byte > NINE)
if byte == DASH
peek_byte = ss.peek_byte
# if it starts with a dash, the next byte must be a digit
return false if peek_byte.nil? || !(peek_byte >= ZERO && peek_byte <= NINE)
end
# The markup could be a float with multiple dots
first_dot_pos = nil
+136 -55
View File
@@ -64,7 +64,7 @@ module Liquid
# @liquid_syntax string | downcase
# @liquid_return [string]
def downcase(input)
input.to_s.downcase
Utils.to_s(input).downcase
end
# @liquid_public_docs
@@ -75,7 +75,7 @@ module Liquid
# @liquid_syntax string | upcase
# @liquid_return [string]
def upcase(input)
input.to_s.upcase
Utils.to_s(input).upcase
end
# @liquid_public_docs
@@ -86,7 +86,7 @@ module Liquid
# @liquid_syntax string | capitalize
# @liquid_return [string]
def capitalize(input)
input.to_s.capitalize
Utils.to_s(input).capitalize
end
# @liquid_public_docs
@@ -97,7 +97,7 @@ module Liquid
# @liquid_syntax string | escape
# @liquid_return [string]
def escape(input)
CGI.escapeHTML(input.to_s) unless input.nil?
CGI.escapeHTML(Utils.to_s(input)) unless input.nil?
end
alias_method :h, :escape
@@ -109,7 +109,7 @@ module Liquid
# @liquid_syntax string | escape_once
# @liquid_return [string]
def escape_once(input)
input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
Utils.to_s(input).gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
end
# @liquid_public_docs
@@ -124,7 +124,7 @@ module Liquid
# @liquid_syntax string | url_encode
# @liquid_return [string]
def url_encode(input)
CGI.escape(input.to_s) unless input.nil?
CGI.escape(Utils.to_s(input)) unless input.nil?
end
# @liquid_public_docs
@@ -138,7 +138,7 @@ module Liquid
def url_decode(input)
return if input.nil?
result = CGI.unescape(input.to_s)
result = CGI.unescape(Utils.to_s(input))
raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
result
@@ -152,7 +152,7 @@ module Liquid
# @liquid_syntax string | base64_encode
# @liquid_return [string]
def base64_encode(input)
Base64.strict_encode64(input.to_s)
Base64.strict_encode64(Utils.to_s(input))
end
# @liquid_public_docs
@@ -163,7 +163,7 @@ module Liquid
# @liquid_syntax string | base64_decode
# @liquid_return [string]
def base64_decode(input)
input = input.to_s
input = Utils.to_s(input)
StandardFilters.try_coerce_encoding(Base64.strict_decode64(input), encoding: input.encoding)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
@@ -177,7 +177,7 @@ module Liquid
# @liquid_syntax string | base64_url_safe_encode
# @liquid_return [string]
def base64_url_safe_encode(input)
Base64.urlsafe_encode64(input.to_s)
Base64.urlsafe_encode64(Utils.to_s(input))
end
# @liquid_public_docs
@@ -188,7 +188,7 @@ module Liquid
# @liquid_syntax string | base64_url_safe_decode
# @liquid_return [string]
def base64_url_safe_decode(input)
input = input.to_s
input = Utils.to_s(input)
StandardFilters.try_coerce_encoding(Base64.urlsafe_decode64(input), encoding: input.encoding)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
@@ -212,7 +212,7 @@ module Liquid
if input.is_a?(Array)
input.slice(offset, length) || []
else
input.to_s.slice(offset, length) || ''
Utils.to_s(input).slice(offset, length) || ''
end
rescue RangeError
if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
@@ -236,10 +236,10 @@ module Liquid
# @liquid_return [string]
def truncate(input, length = 50, truncate_string = "...")
return if input.nil?
input_str = input.to_s
input_str = Utils.to_s(input)
length = Utils.to_integer(length)
truncate_string_str = truncate_string.to_s
truncate_string_str = Utils.to_s(truncate_string)
l = length - truncate_string_str.length
l = 0 if l < 0
@@ -263,7 +263,7 @@ module Liquid
# @liquid_return [string]
def truncatewords(input, words = 15, truncate_string = "...")
return if input.nil?
input = input.to_s
input = Utils.to_s(input)
words = Utils.to_integer(words)
words = 1 if words <= 0
@@ -277,7 +277,8 @@ module Liquid
return input if wordlist.length <= words
wordlist.pop
wordlist.join(" ").concat(truncate_string.to_s)
truncate_string = Utils.to_s(truncate_string)
wordlist.join(" ").concat(truncate_string)
end
# @liquid_public_docs
@@ -288,7 +289,9 @@ module Liquid
# @liquid_syntax string | split: string
# @liquid_return [array[string]]
def split(input, pattern)
input.to_s.split(pattern.to_s)
pattern = Utils.to_s(pattern)
input = Utils.to_s(input)
input.split(pattern)
end
# @liquid_public_docs
@@ -299,7 +302,8 @@ module Liquid
# @liquid_syntax string | strip
# @liquid_return [string]
def strip(input)
input.to_s.strip
input = Utils.to_s(input)
input.strip
end
# @liquid_public_docs
@@ -310,7 +314,8 @@ module Liquid
# @liquid_syntax string | lstrip
# @liquid_return [string]
def lstrip(input)
input.to_s.lstrip
input = Utils.to_s(input)
input.lstrip
end
# @liquid_public_docs
@@ -321,7 +326,8 @@ module Liquid
# @liquid_syntax string | rstrip
# @liquid_return [string]
def rstrip(input)
input.to_s.rstrip
input = Utils.to_s(input)
input.rstrip
end
# @liquid_public_docs
@@ -332,8 +338,9 @@ module Liquid
# @liquid_syntax string | strip_html
# @liquid_return [string]
def strip_html(input)
input = Utils.to_s(input)
empty = ''
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
result = input.gsub(STRIP_HTML_BLOCKS, empty)
result.gsub!(STRIP_HTML_TAGS, empty)
result
end
@@ -346,7 +353,8 @@ module Liquid
# @liquid_syntax string | strip_newlines
# @liquid_return [string]
def strip_newlines(input)
input.to_s.gsub(/\r?\n/, '')
input = Utils.to_s(input)
input.gsub(/\r?\n/, '')
end
# @liquid_public_docs
@@ -357,6 +365,7 @@ module Liquid
# @liquid_syntax array | join
# @liquid_return [string]
def join(input, glue = ' ')
glue = Utils.to_s(glue)
InputIterator.new(input, context).join(glue)
end
@@ -424,29 +433,59 @@ module Liquid
# @liquid_syntax array | where: string, string
# @liquid_return [array[untyped]]
def where(input, property, target_value = nil)
ary = InputIterator.new(input, context)
filter_array(input, property, target_value) { |ary, &block| ary.select(&block) }
end
if ary.empty?
[]
elsif target_value.nil?
ary.select do |item|
item[property]
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
else
ary.select do |item|
item[property] == target_value
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Filters an array to exclude items with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | reject: string, string
# @liquid_return [array[untyped]]
def reject(input, property, target_value = nil)
filter_array(input, property, target_value) { |ary, &block| ary.reject(&block) }
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Tests if any item in an array has a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | some: string, string
# @liquid_return [boolean]
def has(input, property, target_value = nil)
filter_array(input, property, target_value, false) { |ary, &block| ary.any?(&block) }
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the first item in an array with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | find: string, string
# @liquid_return [untyped]
def find(input, property, target_value = nil)
filter_array(input, property, target_value, nil) { |ary, &block| ary.find(&block) }
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the index of the first item in an array with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | find_index: string, string
# @liquid_return [number]
def find_index(input, property, target_value = nil)
filter_array(input, property, target_value, nil) { |ary, &block| ary.find_index(&block) }
end
# @liquid_public_docs
@@ -543,7 +582,10 @@ module Liquid
# @liquid_syntax string | replace: string, string
# @liquid_return [string]
def replace(input, string, replacement = '')
input.to_s.gsub(string.to_s, replacement.to_s)
string = Utils.to_s(string)
replacement = Utils.to_s(replacement)
input = Utils.to_s(input)
input.gsub(string, replacement)
end
# @liquid_public_docs
@@ -554,7 +596,10 @@ module Liquid
# @liquid_syntax string | replace_first: string, string
# @liquid_return [string]
def replace_first(input, string, replacement = '')
input.to_s.sub(string.to_s, replacement.to_s)
string = Utils.to_s(string)
replacement = Utils.to_s(replacement)
input = Utils.to_s(input)
input.sub(string, replacement)
end
# @liquid_public_docs
@@ -565,9 +610,9 @@ module Liquid
# @liquid_syntax string | replace_last: string, string
# @liquid_return [string]
def replace_last(input, string, replacement)
input = input.to_s
string = string.to_s
replacement = replacement.to_s
input = Utils.to_s(input)
string = Utils.to_s(string)
replacement = Utils.to_s(replacement)
start_index = input.rindex(string)
@@ -619,7 +664,9 @@ module Liquid
# @liquid_syntax string | append: string
# @liquid_return [string]
def append(input, string)
input.to_s + string.to_s
input = Utils.to_s(input)
string = Utils.to_s(string)
input + string
end
# @liquid_public_docs
@@ -648,7 +695,9 @@ module Liquid
# @liquid_syntax string | prepend: string
# @liquid_return [string]
def prepend(input, string)
string.to_s + input.to_s
input = Utils.to_s(input)
string = Utils.to_s(string)
string + input
end
# @liquid_public_docs
@@ -659,7 +708,8 @@ module Liquid
# @liquid_syntax string | newline_to_br
# @liquid_return [string]
def newline_to_br(input)
input.to_s.gsub(/\r?\n/, "<br />\n")
input = Utils.to_s(input)
input.gsub(/\r?\n/, "<br />\n")
end
# Reformat a date using Ruby's core Time#strftime( string ) -> string
@@ -694,11 +744,12 @@ module Liquid
#
# See also: http://www.ruby-doc.org/core/Time.html#method-i-strftime
def date(input, format)
return input if format.to_s.empty?
str_format = Utils.to_s(format)
return input if str_format.empty?
return input unless (date = Utils.to_date(input))
date.strftime(format.to_s)
date.strftime(str_format)
end
# @liquid_public_docs
@@ -918,6 +969,25 @@ module Liquid
attr_reader :context
def filter_array(input, property, target_value, default_value = [], &block)
ary = InputIterator.new(input, context)
return default_value if ary.empty?
block.call(ary) do |item|
if target_value.nil?
item[property]
else
item[property] == target_value
end
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
def raise_property_error(property)
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
end
@@ -968,7 +1038,18 @@ module Liquid
end
def join(glue)
to_a.join(glue.to_s)
first = true
output = +""
each do |item|
if first
first = false
else
output << glue
end
output << Liquid::Utils.to_s(item)
end
output
end
def concat(args)
+96
View File
@@ -89,5 +89,101 @@ module Liquid
# Otherwise return the object itself
obj
end
def self.to_s(obj, seen = {})
case obj
when Hash
# If the custom hash implementation overrides `#to_s`, use their
# custom implementation. Otherwise we use Liquid's default
# implementation.
if obj.class.instance_method(:to_s) == HASH_TO_S_METHOD
hash_inspect(obj, seen)
else
obj.to_s
end
when Array
array_inspect(obj, seen)
else
obj.to_s
end
end
def self.inspect(obj, seen = {})
case obj
when Hash
# If the custom hash implementation overrides `#inspect`, use their
# custom implementation. Otherwise we use Liquid's default
# implementation.
if obj.class.instance_method(:inspect) == HASH_INSPECT_METHOD
hash_inspect(obj, seen)
else
obj.inspect
end
when Array
array_inspect(obj, seen)
else
obj.inspect
end
end
def self.array_inspect(arr, seen = {})
if seen[arr.object_id]
return "[...]"
end
seen[arr.object_id] = true
str = +"["
cursor = 0
len = arr.length
while cursor < len
if cursor > 0
str << ", "
end
item_str = inspect(arr[cursor], seen)
str << item_str
cursor += 1
end
str << "]"
str
ensure
seen.delete(arr.object_id)
end
def self.hash_inspect(hash, seen = {})
if seen[hash.object_id]
return "{...}"
end
seen[hash.object_id] = true
str = +"{"
first = true
hash.each do |key, value|
if first
first = false
else
str << ", "
end
key_str = inspect(key, seen)
str << key_str
str << "=>"
value_str = inspect(value, seen)
str << value_str
end
str << "}"
str
ensure
seen.delete(hash.object_id)
end
HASH_TO_S_METHOD = Hash.instance_method(:to_s)
private_constant :HASH_TO_S_METHOD
HASH_INSPECT_METHOD = Hash.instance_method(:inspect)
private_constant :HASH_INSPECT_METHOD
end
end
+2 -2
View File
@@ -107,8 +107,8 @@ module Liquid
obj.each do |o|
render_obj_to_output(o, output)
end
when
output << obj.to_s
else
output << Liquid::Utils.to_s(obj)
end
end
+1 -1
View File
@@ -6,7 +6,7 @@ module Liquid
attr_reader :name, :lookups
def self.parse(markup, string_scanner, cache = nil)
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
new(markup, string_scanner, cache)
end
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true
module Liquid
VERSION = "5.6.1"
VERSION = "5.7.2"
end
+9
View File
@@ -26,7 +26,16 @@ class ExpressionTest < Minitest::Test
def test_float
assert_template_result("-17.42", "{{ -17.42 }}")
assert_template_result("2.5", "{{ 2.5 }}")
assert_expression_result(0.0, "0.....5")
assert_expression_result(0.0, "-0..1")
assert_expression_result(1.5, "1.5")
# this is a unfortunate quirky behavior of Liquid
result = Expression.parse(".5")
assert_kind_of(Liquid::VariableLookup, result)
result = Expression.parse("-.5")
assert_kind_of(Liquid::VariableLookup, result)
end
def test_range
+106
View File
@@ -0,0 +1,106 @@
# frozen_string_literal: true
require 'test_helper'
class HashRenderingTest < Minitest::Test
def test_render_empty_hash
assert_template_result("{}", "{{ my_hash }}", { "my_hash" => {} })
end
def test_render_hash_with_string_keys_and_values
assert_template_result("{\"key1\"=>\"value1\", \"key2\"=>\"value2\"}", "{{ my_hash }}", { "my_hash" => { "key1" => "value1", "key2" => "value2" } })
end
def test_render_hash_with_symbol_keys_and_integer_values
assert_template_result("{:key1=>1, :key2=>2}", "{{ my_hash }}", { "my_hash" => { key1: 1, key2: 2 } })
end
def test_render_nested_hash
assert_template_result("{\"outer\"=>{\"inner\"=>\"value\"}}", "{{ my_hash }}", { "my_hash" => { "outer" => { "inner" => "value" } } })
end
def test_render_hash_with_array_values
assert_template_result("{\"numbers\"=>[1, 2, 3]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [1, 2, 3] } })
end
def test_render_recursive_hash
recursive_hash = { "self" => {} }
recursive_hash["self"]["self"] = recursive_hash
assert_template_result("{\"self\"=>{\"self\"=>{...}}}", "{{ my_hash }}", { "my_hash" => recursive_hash })
end
def test_hash_with_downcase_filter
assert_template_result("{\"key\"=>\"value\", \"anotherkey\"=>\"anothervalue\"}", "{{ my_hash | downcase }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_upcase_filter
assert_template_result("{\"KEY\"=>\"VALUE\", \"ANOTHERKEY\"=>\"ANOTHERVALUE\"}", "{{ my_hash | upcase }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_strip_filter
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | strip }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_escape_filter
assert_template_result("{&quot;Key&quot;=&gt;&quot;Value&quot;, &quot;AnotherKey&quot;=&gt;&quot;AnotherValue&quot;}", "{{ my_hash | escape }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_url_encode_filter
assert_template_result("%7B%22Key%22%3D%3E%22Value%22%2C+%22AnotherKey%22%3D%3E%22AnotherValue%22%7D", "{{ my_hash | url_encode }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_strip_html_filter
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | strip_html }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_truncate__20_filter
assert_template_result("{\"Key\"=>\"Value\", ...", "{{ my_hash | truncate: 20 }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_replace___key____replaced_key__filter
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | replace: 'key', 'replaced_key' }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_append____appended_text__filter
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"} appended text", "{{ my_hash | append: ' appended text' }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_prepend___prepended_text___filter
assert_template_result("prepended text {\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | prepend: 'prepended text ' }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_render_hash_with_array_values_empty
assert_template_result("{\"numbers\"=>[]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [] } })
end
def test_render_hash_with_array_values_hash
assert_template_result("{\"numbers\"=>[{:foo=>42}]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [{ foo: 42 }] } })
end
def test_join_filter_with_hash
array = [{ "key1" => "value1" }, { "key2" => "value2" }]
glue = { "lol" => "wut" }
assert_template_result("{\"key1\"=>\"value1\"}{\"lol\"=>\"wut\"}{\"key2\"=>\"value2\"}", "{{ my_array | join: glue }}", { "my_array" => array, "glue" => glue })
end
def test_render_hash_with_hash_key
assert_template_result("{{\"foo\"=>\"bar\"}=>42}", "{{ my_hash }}", { "my_hash" => { Hash["foo" => "bar"] => 42 } })
end
def test_rendering_hash_with_custom_to_s_method_uses_custom_to_s
my_hash = Class.new(Hash) do
def to_s
"kewl"
end
end.new
assert_template_result("kewl", "{{ my_hash }}", { "my_hash" => my_hash })
end
def test_rendering_hash_without_custom_to_s_uses_default_inspect
my_hash = Class.new(Hash).new
my_hash[:foo] = :bar
assert_template_result("{:foo=>:bar}", "{{ my_hash }}", { "my_hash" => my_hash })
end
end
+222 -5
View File
@@ -133,6 +133,18 @@ class StandardFiltersTest < Minitest::Test
assert_equal([], @filters.slice(input, -(1 << 63), 6))
end
def test_find_on_empty_array
assert_nil(@filters.find([], 'foo', 'bar'))
end
def test_find_index_on_empty_array
assert_nil(@filters.find_index([], 'foo', 'bar'))
end
def test_has_on_empty_array
refute(@filters.has([], 'foo', 'bar'))
end
def test_truncate
assert_equal('1234...', @filters.truncate('1234567890', 7))
assert_equal('1234567890', @filters.truncate('1234567890', 20))
@@ -281,6 +293,16 @@ class StandardFiltersTest < Minitest::Test
assert_equal('1121314', @filters.join([1, 2, 3, 4], 1))
end
def test_join_calls_to_liquid_on_each_element
drop = Class.new(Liquid::Drop) do
def to_liquid
'i did it'
end
end
assert_equal('i did it, i did it', @filters.join([drop.new, drop.new], ", "))
end
def test_sort
assert_equal([1, 2, 3, 4], @filters.sort([4, 3, 2, 1]))
assert_equal([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
@@ -827,21 +849,216 @@ class StandardFiltersTest < Minitest::Test
assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
end
def test_where
input = [
def test_reject
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
expectation = [
template = "{{ array | reject: 'ok' | map: 'handle' | join: ' ' }}"
expected_output = "beta gamma"
assert_template_result(expected_output, template, { "array" => array })
end
def test_reject_with_value
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
assert_equal(expectation, @filters.where(input, "ok", true))
assert_equal(expectation, @filters.where(input, "ok"))
template = "{{ array | reject: 'ok', true | map: 'handle' | join: ' ' }}"
expected_output = "beta gamma"
assert_template_result(expected_output, template, { "array" => array })
end
def test_reject_with_false_value
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | reject: 'ok', false | map: 'handle' | join: ' ' }}"
expected_output = "alpha delta"
assert_template_result(expected_output, template, { "array" => array })
end
def test_has
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => false },
]
expected_output = "true"
assert_template_result(expected_output, "{{ array | has: 'ok' }}", { "array" => array })
assert_template_result(expected_output, "{{ array | has: 'ok', true }}", { "array" => array })
end
def test_has_when_does_not_have_it
array = [
{ "handle" => "alpha", "ok" => false },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => false },
]
expected_output = "false"
assert_template_result(expected_output, "{{ array | has: 'ok' }}", { "array" => array })
assert_template_result(expected_output, "{{ array | has: 'ok', true }}", { "array" => array })
end
def test_has_with_empty_arrays
template = <<~LIQUID
{%- assign has_product = products | has: 'title.content', 'Not found' -%}
{%- unless has_product -%}
Product not found.
{%- endunless -%}
LIQUID
expected_output = "Product not found."
assert_template_result(expected_output, template, { "products" => [] })
end
def test_has_with_false_value
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | has: 'ok', false }}"
expected_output = "true"
assert_template_result(expected_output, template, { "array" => array })
end
def test_has_with_false_value_when_does_not_have_it
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => true },
{ "handle" => "gamma", "ok" => true },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | has: 'ok', false }}"
expected_output = "false"
assert_template_result(expected_output, template, { "array" => array })
end
def test_find_with_value
products = [
{ "title" => "Pro goggles", "price" => 1299 },
{ "title" => "Thermal gloves", "price" => 1499 },
{ "title" => "Alpine jacket", "price" => 3999 },
{ "title" => "Mountain boots", "price" => 3899 },
{ "title" => "Safety helmet", "price" => 1999 }
]
template = <<~LIQUID
{%- assign product = products | find: 'price', 3999 -%}
{{- product.title -}}
LIQUID
expected_output = "Alpine jacket"
assert_template_result(expected_output, template, { "products" => products })
end
def test_find_with_empty_arrays
template = <<~LIQUID
{%- assign product = products | find: 'title.content', 'Not found' -%}
{%- unless product -%}
Product not found.
{%- endunless -%}
LIQUID
expected_output = "Product not found."
assert_template_result(expected_output, template, { "products" => [] })
end
def test_find_index_with_value
products = [
{ "title" => "Pro goggles", "price" => 1299 },
{ "title" => "Thermal gloves", "price" => 1499 },
{ "title" => "Alpine jacket", "price" => 3999 },
{ "title" => "Mountain boots", "price" => 3899 },
{ "title" => "Safety helmet", "price" => 1999 }
]
template = <<~LIQUID
{%- assign index = products | find_index: 'price', 3999 -%}
{{- index -}}
LIQUID
expected_output = "2"
assert_template_result(expected_output, template, { "products" => products })
end
def test_find_index_with_empty_arrays
template = <<~LIQUID
{%- assign index = products | find_index: 'title.content', 'Not found' -%}
{%- unless index -%}
Index not found.
{%- endunless -%}
LIQUID
expected_output = "Index not found."
assert_template_result(expected_output, template, { "products" => [] })
end
def test_where
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | where: 'ok' | map: 'handle' | join: ' ' }}"
expected_output = "alpha delta"
assert_template_result(expected_output, template, { "array" => array })
end
def test_where_with_value
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | where: 'ok', true | map: 'handle' | join: ' ' }}"
expected_output = "alpha delta"
assert_template_result(expected_output, template, { "array" => array })
end
def test_where_with_false_value
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | where: 'ok', false | map: 'handle' | join: ' ' }}"
expected_output = "beta gamma"
assert_template_result(expected_output, template, { "array" => array })
end
def test_where_string_keys