mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-13 07:50:43 -07:00
Compare commits
71
Commits
inline-comment
...
v5.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c60c3c7802 | ||
|
|
11625b1bc9 | ||
|
|
ec6fb4d5fa | ||
|
|
7357dcf185 | ||
|
|
68c3827ef2 | ||
|
|
4af38bc549 | ||
|
|
df241abf70 | ||
|
|
10f8337209 | ||
|
|
5ed0410a8b | ||
|
|
0f5220c391 | ||
|
|
7a23f46fab | ||
|
|
3f7edf00b9 | ||
|
|
b4a2a79e26 | ||
|
|
1d2bee1f60 | ||
|
|
01e6eec97a | ||
|
|
fbdab19358 | ||
|
|
ce85ac5d3d | ||
|
|
c0ffee16a3 | ||
|
|
a7eb33fa39 | ||
|
|
1a85e98793 | ||
|
|
c588337aac | ||
|
|
97f7922457 | ||
|
|
0d83e64cfe | ||
|
|
0d5e01ae98 | ||
|
|
15eaa49e48 | ||
|
|
91c54c579d | ||
|
|
1310c4978d | ||
|
|
3de1db3c3a | ||
|
|
03522caaf8 | ||
|
|
7acea2a9c9 | ||
|
|
d8ef698539 | ||
|
|
ebdfdb80e5 | ||
|
|
95e9fa5010 | ||
|
|
db3999a008 | ||
|
|
10e2aa8d5b | ||
|
|
7c4114671b | ||
|
|
b01de9d325 | ||
|
|
a5369c26a8 | ||
|
|
a03de8f9ea | ||
|
|
e86fe27259 | ||
|
|
c8906d05b9 | ||
|
|
50d1a2ffc9 | ||
|
|
f686c5dec7 | ||
|
|
aa8ce87b96 | ||
|
|
698f5e0d96 | ||
|
|
996bfe0c82 | ||
|
|
be81c9ae5a | ||
|
|
edd4d70aee | ||
|
|
ac66dbbafe | ||
|
|
017c1b5e83 | ||
|
|
250555c9a8 | ||
|
|
e361a4d53c | ||
|
|
b9e0d28729 | ||
|
|
020f6b93c5 | ||
|
|
cfe1637bdd | ||
|
|
eab13a07d9 | ||
|
|
ca96ca0fef | ||
|
|
4e7a953e73 | ||
|
|
6ac2499f7f | ||
|
|
ff70161512 | ||
|
|
026157e128 | ||
|
|
bf64239ea6 | ||
|
|
c270a6f378 | ||
|
|
4fba61a802 | ||
|
|
6b6baece25 | ||
|
|
15b2d193ec | ||
|
|
fd712d134a | ||
|
|
0c2db998cf | ||
|
|
9dac68cce1 | ||
|
|
c50509b741 | ||
|
|
cd66572514 |
@@ -7,7 +7,7 @@ jobs:
|
||||
matrix:
|
||||
entry:
|
||||
- { ruby: 2.5, allowed-failure: false } # minimum supported
|
||||
- { ruby: 3.0, allowed-failure: false } # latest
|
||||
- { ruby: 3.1, allowed-failure: false } # latest
|
||||
- { ruby: ruby-head, allowed-failure: true }
|
||||
name: test (${{ matrix.entry.ruby }})
|
||||
steps:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+8
-2
@@ -1,5 +1,7 @@
|
||||
inherit_gem:
|
||||
rubocop-shopify: rubocop.yml
|
||||
|
||||
inherit_from:
|
||||
- 'https://shopify.github.io/ruby-style-guide/rubocop.yml'
|
||||
- .rubocop_todo.yml
|
||||
|
||||
require: rubocop-performance
|
||||
@@ -8,7 +10,7 @@ Performance:
|
||||
Enabled: true
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.4
|
||||
TargetRubyVersion: 2.5
|
||||
NewCops: disable
|
||||
Exclude:
|
||||
- 'vendor/bundle/**/*'
|
||||
@@ -16,3 +18,7 @@ AllCops:
|
||||
Naming/MethodName:
|
||||
Exclude:
|
||||
- 'example/server/liquid_servlet.rb'
|
||||
|
||||
# Backport https://github.com/Shopify/ruby-style-guide/pull/258
|
||||
Layout/BeginEndAlignment:
|
||||
Enabled: true
|
||||
|
||||
+4
-1
@@ -5,6 +5,7 @@
|
||||
* Bugfixes
|
||||
* Performance improvements
|
||||
* Features that are likely to be useful to the majority of Liquid users
|
||||
* Documentation updates that are concise and likely to be useful to the majority of Liquid users
|
||||
|
||||
## Things we won't merge
|
||||
|
||||
@@ -14,12 +15,14 @@
|
||||
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
|
||||
* Code that does not include tests
|
||||
* Code that breaks existing tests
|
||||
* Documentation changes that are verbose, incorrect or not important to most people (we want to keep it simple and easy to understand)
|
||||
|
||||
## Workflow
|
||||
|
||||
* [Sign the CLA](https://cla.shopify.com/) if you haven't already
|
||||
* Fork the Liquid repository
|
||||
* Create a new branch in your fork
|
||||
* For updating [Liquid documentation](https://shopify.github.io/liquid/), create it from `gh-pages` branch. (You can skip tests.)
|
||||
* 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
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ end
|
||||
|
||||
group :test do
|
||||
gem 'rubocop', '~> 1.4', require: false
|
||||
gem 'rubocop-shopify', '~> 1.0.7', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
|
||||
platform :mri, :truffleruby do
|
||||
|
||||
+41
@@ -1,5 +1,46 @@
|
||||
# Liquid Change Log
|
||||
|
||||
## 5.3.0 2022-03-22
|
||||
|
||||
### Fixes
|
||||
* StandardFilter: Fix missing @context on iterations (#1525) [Thierry Joyal]
|
||||
* Test under Ruby 3.1 (#1533) [petergoldstein]
|
||||
* Fix warning about block and default value in `static_registers.rb` (#1531) [Peter Zhu]
|
||||
|
||||
### Deprecation
|
||||
* Condition#evaluate to require mandatory context argument in Liquid 6.0.0 (#1527) [Thierry Joyal]
|
||||
|
||||
## 5.2.0 2022-03-01
|
||||
|
||||
### Features
|
||||
* Add `remove_last`, and `replace_last` filters (#1422) [Anders Hagbard]
|
||||
* Eagerly cache global filters (#1524) [Jean Boussier]
|
||||
|
||||
### Fixes
|
||||
* Fix some internal errors in filters from invalid input (#1476) [Dylan Thacker-Smith]
|
||||
* Allow dash in filter kwarg name for consistency with Liquid::C (#1518) [CP Clermont]
|
||||
|
||||
## 5.1.0 / 2021-09-09
|
||||
|
||||
### Features
|
||||
* Add `base64_encode`, `base64_decode`, `base64_url_safe_encode`, and `base64_url_safe_decode` filters (#1450) [Daniel Insley]
|
||||
* Introduce `to_liquid_value` in `Liquid::Drop` (#1441) [Michael Go]
|
||||
|
||||
### Fixes
|
||||
* Fix support for using a String subclass for the liquid source (#1421) [Dylan Thacker-Smith]
|
||||
* Add `ParseTreeVisitor` to `RangeLookup` (#1470) [CP Clermont]
|
||||
* Translate `RangeError` to `Liquid::Error` for `truncatewords` with large int (#1431) [Dylan Thacker-Smith]
|
||||
|
||||
## 5.0.1 / 2021-03-24
|
||||
|
||||
### Fixes
|
||||
* Add ParseTreeVisitor to Echo tag (#1414) [CP Clermont]
|
||||
* Test with ruby 3.0 as the latest ruby version (#1398) [Dylan Thacker-Smith]
|
||||
* Handle carriage return in newlines_to_br (#1391) [Unending]
|
||||
|
||||
### Performance Improvements
|
||||
* Use split limit in truncatewords (#1361) [Dylan Thacker-Smith]
|
||||
|
||||
## 5.0.0 / 2021-01-06
|
||||
|
||||
### Features
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
* [Contributing guidelines](CONTRIBUTING.md)
|
||||
* [Version history](History.md)
|
||||
* [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
|
||||
* [Liquid documentation from Shopify](https://shopify.dev/api/liquid)
|
||||
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
|
||||
* [Website](http://liquidmarkup.org/)
|
||||
|
||||
@@ -56,7 +56,7 @@ For standard use you can just pass it the content of a file and call render with
|
||||
|
||||
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
|
||||
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
|
||||
it very hard to debug and can lead to unexpected behaviour.
|
||||
it very hard to debug and can lead to unexpected behaviour.
|
||||
|
||||
Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
|
||||
when templates are invalid. You can enable this new parser like this:
|
||||
|
||||
+2
-2
@@ -36,7 +36,7 @@ module Liquid
|
||||
VariableIncompleteEnd = /\}\}?/
|
||||
QuotedString = /"[^"]*"|'[^']*'/
|
||||
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
|
||||
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
|
||||
TagAttributes = /(\w[\w-]*)\s*\:\s*(#{QuotedFragment})/o
|
||||
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
|
||||
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
|
||||
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
|
||||
@@ -59,8 +59,8 @@ require 'liquid/forloop_drop'
|
||||
require 'liquid/extensions'
|
||||
require 'liquid/errors'
|
||||
require 'liquid/interrupts'
|
||||
require 'liquid/strainer_factory'
|
||||
require 'liquid/strainer_template'
|
||||
require 'liquid/strainer_factory'
|
||||
require 'liquid/expression'
|
||||
require 'liquid/context'
|
||||
require 'liquid/parser_switching'
|
||||
|
||||
@@ -231,8 +231,8 @@ module Liquid
|
||||
end
|
||||
|
||||
def create_variable(token, parse_context)
|
||||
token.scan(ContentOfVariable) do |content|
|
||||
markup = content.first
|
||||
if token =~ ContentOfVariable
|
||||
markup = Regexp.last_match(1)
|
||||
return Variable.new(markup, parse_context)
|
||||
end
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
|
||||
+10
-4
@@ -8,7 +8,7 @@ module Liquid
|
||||
# c = Condition.new(1, '==', 1)
|
||||
# c.evaluate #=> true
|
||||
#
|
||||
class Condition #:nodoc:
|
||||
class Condition # :nodoc:
|
||||
@@operators = {
|
||||
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
|
||||
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
@@ -61,7 +61,7 @@ module Liquid
|
||||
@child_condition = nil
|
||||
end
|
||||
|
||||
def evaluate(context = Context.new)
|
||||
def evaluate(context = deprecated_default_context)
|
||||
condition = self
|
||||
result = nil
|
||||
loop do
|
||||
@@ -134,8 +134,8 @@ module Liquid
|
||||
# return this as the result.
|
||||
return context.evaluate(left) if op.nil?
|
||||
|
||||
left = context.evaluate(left)
|
||||
right = context.evaluate(right)
|
||||
left = Liquid::Utils.to_liquid_value(context.evaluate(left))
|
||||
right = Liquid::Utils.to_liquid_value(context.evaluate(right))
|
||||
|
||||
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
|
||||
|
||||
@@ -150,6 +150,12 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def deprecated_default_context
|
||||
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
|
||||
" and will be removed from Liquid 6.0.0.")
|
||||
Context.new
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[
|
||||
|
||||
@@ -124,7 +124,7 @@ module Liquid
|
||||
# context['var'] = 'hi'
|
||||
# end
|
||||
#
|
||||
# context['var] #=> nil
|
||||
# context['var'] #=> nil
|
||||
def stack(new_scope = {})
|
||||
push(new_scope)
|
||||
yield
|
||||
|
||||
+11
-10
@@ -10,21 +10,23 @@ module Liquid
|
||||
'empty' => ''
|
||||
}.freeze
|
||||
|
||||
SINGLE_QUOTED_STRING = /\A\s*'(.*)'\s*\z/m
|
||||
DOUBLE_QUOTED_STRING = /\A\s*"(.*)"\s*\z/m
|
||||
INTEGERS_REGEX = /\A\s*(-?\d+)\s*\z/
|
||||
FLOATS_REGEX = /\A\s*(-?\d[\d\.]+)\s*\z/
|
||||
INTEGERS_REGEX = /\A(-?\d+)\z/
|
||||
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
|
||||
|
||||
# Use an atomic group (?>...) to avoid pathological backtracing from
|
||||
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
|
||||
RANGES_REGEX = /\A\s*\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\s*\z/
|
||||
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
|
||||
|
||||
def self.parse(markup)
|
||||
return nil unless markup
|
||||
|
||||
markup = markup.strip
|
||||
if (markup.start_with?('"') && markup.end_with?('"')) ||
|
||||
(markup.start_with?("'") && markup.end_with?("'"))
|
||||
return markup[1..-2]
|
||||
end
|
||||
|
||||
case markup
|
||||
when nil
|
||||
nil
|
||||
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
|
||||
Regexp.last_match(1)
|
||||
when INTEGERS_REGEX
|
||||
Regexp.last_match(1).to_i
|
||||
when RANGES_REGEX
|
||||
@@ -32,7 +34,6 @@ module Liquid
|
||||
when FLOATS_REGEX
|
||||
Regexp.last_match(1).to_f
|
||||
else
|
||||
markup = markup.strip
|
||||
if LITERALS.key?(markup)
|
||||
LITERALS[markup]
|
||||
else
|
||||
|
||||
@@ -12,6 +12,8 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :start_obj, :end_obj
|
||||
|
||||
def initialize(start_obj, end_obj)
|
||||
@start_obj = start_obj
|
||||
@end_obj = end_obj
|
||||
@@ -35,5 +37,11 @@ module Liquid
|
||||
Utils.to_integer(input)
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.start_obj, @node.end_obj]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'cgi'
|
||||
require 'base64'
|
||||
require 'bigdecimal'
|
||||
|
||||
module Liquid
|
||||
module StandardFilters
|
||||
MAX_INT = (1 << 31) - 1
|
||||
HTML_ESCAPE = {
|
||||
'&' => '&',
|
||||
'>' => '>',
|
||||
@@ -62,6 +64,26 @@ module Liquid
|
||||
result
|
||||
end
|
||||
|
||||
def base64_encode(input)
|
||||
Base64.strict_encode64(input.to_s)
|
||||
end
|
||||
|
||||
def base64_decode(input)
|
||||
Base64.strict_decode64(input.to_s)
|
||||
rescue ::ArgumentError
|
||||
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
|
||||
end
|
||||
|
||||
def base64_url_safe_encode(input)
|
||||
Base64.urlsafe_encode64(input.to_s)
|
||||
end
|
||||
|
||||
def base64_url_safe_decode(input)
|
||||
Base64.urlsafe_decode64(input.to_s)
|
||||
rescue ::ArgumentError
|
||||
raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
|
||||
end
|
||||
|
||||
def slice(input, offset, length = nil)
|
||||
offset = Utils.to_integer(offset)
|
||||
length = length ? Utils.to_integer(length) : 1
|
||||
@@ -93,7 +115,13 @@ module Liquid
|
||||
words = Utils.to_integer(words)
|
||||
words = 1 if words <= 0
|
||||
|
||||
wordlist = input.split(" ", words + 1)
|
||||
wordlist = begin
|
||||
input.split(" ", words + 1)
|
||||
rescue RangeError
|
||||
raise if words + 1 < MAX_INT
|
||||
# e.g. integer #{words} too big to convert to `int'
|
||||
raise Liquid::ArgumentError, "integer #{words} too big for truncatewords"
|
||||
end
|
||||
return input if wordlist.length <= words
|
||||
|
||||
wordlist.pop
|
||||
@@ -185,17 +213,23 @@ module Liquid
|
||||
|
||||
if ary.empty?
|
||||
[]
|
||||
elsif ary.first.respond_to?(:[]) && target_value.nil?
|
||||
begin
|
||||
ary.select { |item| item[property] }
|
||||
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
|
||||
elsif ary.first.respond_to?(:[])
|
||||
begin
|
||||
ary.select { |item| item[property] == target_value }
|
||||
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
|
||||
end
|
||||
@@ -209,11 +243,14 @@ module Liquid
|
||||
ary.uniq
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
[]
|
||||
elsif ary.first.respond_to?(:[])
|
||||
begin
|
||||
ary.uniq { |a| a[property] }
|
||||
else
|
||||
ary.uniq do |item|
|
||||
item[property]
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -249,11 +286,14 @@ module Liquid
|
||||
ary.compact
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
[]
|
||||
elsif ary.first.respond_to?(:[])
|
||||
begin
|
||||
ary.reject { |a| a[property].nil? }
|
||||
else
|
||||
ary.reject do |item|
|
||||
item[property].nil?
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -268,14 +308,34 @@ module Liquid
|
||||
input.to_s.sub(string.to_s, replacement.to_s)
|
||||
end
|
||||
|
||||
# Replace the last occurrences of a string with another
|
||||
def replace_last(input, string, replacement)
|
||||
input = input.to_s
|
||||
string = string.to_s
|
||||
replacement = replacement.to_s
|
||||
|
||||
start_index = input.rindex(string)
|
||||
|
||||
return input unless start_index
|
||||
|
||||
output = input.dup
|
||||
output[start_index, string.length] = replacement
|
||||
output
|
||||
end
|
||||
|
||||
# remove a substring
|
||||
def remove(input, string)
|
||||
input.to_s.gsub(string.to_s, '')
|
||||
replace(input, string, '')
|
||||
end
|
||||
|
||||
# remove the first occurrences of a substring
|
||||
def remove_first(input, string)
|
||||
input.to_s.sub(string.to_s, '')
|
||||
replace_first(input, string, '')
|
||||
end
|
||||
|
||||
# remove the last occurences of a substring
|
||||
def remove_last(input, string)
|
||||
replace_last(input, string, '')
|
||||
end
|
||||
|
||||
# add one string to another
|
||||
@@ -440,7 +500,7 @@ module Liquid
|
||||
#
|
||||
def default(input, default_value = '', options = {})
|
||||
options = {} unless options.is_a?(Hash)
|
||||
false_check = options['allow_false'] ? input.nil? : !input
|
||||
false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input)
|
||||
false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
|
||||
end
|
||||
|
||||
@@ -458,10 +518,16 @@ module Liquid
|
||||
end
|
||||
|
||||
def nil_safe_compare(a, b)
|
||||
if !a.nil? && !b.nil?
|
||||
a <=> b
|
||||
result = a <=> b
|
||||
|
||||
if result
|
||||
result
|
||||
elsif a.nil?
|
||||
1
|
||||
elsif b.nil?
|
||||
-1
|
||||
else
|
||||
a.nil? ? 1 : -1
|
||||
raise Liquid::ArgumentError, "cannot sort values of incompatible types"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -516,8 +582,9 @@ module Liquid
|
||||
|
||||
def each
|
||||
@input.each do |e|
|
||||
e = e.respond_to?(:to_liquid) ? e.to_liquid : e
|
||||
e.context = @context if e.respond_to?(:context=)
|
||||
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
|
||||
yield(e)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,7 +31,11 @@ module Liquid
|
||||
if @registers.key?(key)
|
||||
@registers.fetch(key)
|
||||
elsif default != UNDEFINED
|
||||
@static.fetch(key, default, &block)
|
||||
if block_given?
|
||||
@static.fetch(key, &block)
|
||||
else
|
||||
@static.fetch(key, default)
|
||||
end
|
||||
else
|
||||
@static.fetch(key, &block)
|
||||
end
|
||||
|
||||
@@ -7,25 +7,26 @@ module Liquid
|
||||
|
||||
def add_global_filter(filter)
|
||||
strainer_class_cache.clear
|
||||
global_filters << filter
|
||||
GlobalCache.add_filter(filter)
|
||||
end
|
||||
|
||||
def create(context, filters = [])
|
||||
strainer_from_cache(filters).new(context)
|
||||
end
|
||||
|
||||
GlobalCache = Class.new(StrainerTemplate)
|
||||
|
||||
private
|
||||
|
||||
def global_filters
|
||||
@global_filters ||= []
|
||||
end
|
||||
|
||||
def strainer_from_cache(filters)
|
||||
strainer_class_cache[filters] ||= begin
|
||||
klass = Class.new(StrainerTemplate)
|
||||
global_filters.each { |f| klass.add_filter(f) }
|
||||
filters.each { |f| klass.add_filter(f) }
|
||||
klass
|
||||
if filters.empty?
|
||||
GlobalCache
|
||||
else
|
||||
strainer_class_cache[filters] ||= begin
|
||||
klass = Class.new(GlobalCache)
|
||||
filters.each { |f| klass.add_filter(f) }
|
||||
klass
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -31,6 +31,11 @@ module Liquid
|
||||
filter_methods.include?(method.to_s)
|
||||
end
|
||||
|
||||
def inherited(subclass)
|
||||
super
|
||||
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_methods
|
||||
|
||||
@@ -52,7 +52,14 @@ module Liquid
|
||||
@blocks.each do |block|
|
||||
if block.else?
|
||||
block.attachment.render_to_output_buffer(context, output) if execute_else_block
|
||||
elsif block.evaluate(context)
|
||||
next
|
||||
end
|
||||
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
block.evaluate(context)
|
||||
)
|
||||
|
||||
if result
|
||||
execute_else_block = false
|
||||
block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
|
||||
@@ -12,6 +12,8 @@ module Liquid
|
||||
# {% echo user | link %}
|
||||
#
|
||||
class Echo < Tag
|
||||
attr_reader :variable
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
@variable = Variable.new(markup, parse_context)
|
||||
@@ -20,6 +22,12 @@ module Liquid
|
||||
def render(context)
|
||||
@variable.render_to_output_buffer(context, +'')
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.variable]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('echo', Echo)
|
||||
|
||||
@@ -50,7 +50,11 @@ module Liquid
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@blocks.each do |block|
|
||||
if block.evaluate(context)
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
block.evaluate(context)
|
||||
)
|
||||
|
||||
if result
|
||||
return block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,13 +11,21 @@ module Liquid
|
||||
def render_to_output_buffer(context, output)
|
||||
# First condition is interpreted backwards ( if not )
|
||||
first_block = @blocks.first
|
||||
unless first_block.evaluate(context)
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
first_block.evaluate(context)
|
||||
)
|
||||
|
||||
unless result
|
||||
return first_block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
|
||||
# After the first condition unless works just like if
|
||||
@blocks[1..-1].each do |block|
|
||||
if block.evaluate(context)
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
block.evaluate(context)
|
||||
)
|
||||
|
||||
if result
|
||||
return block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ module Liquid
|
||||
attr_reader :line_number, :for_liquid_tag
|
||||
|
||||
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
||||
@source = source
|
||||
@source = source.to_s.to_str
|
||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||
@for_liquid_tag = for_liquid_tag
|
||||
@tokens = tokenize
|
||||
@@ -24,7 +24,7 @@ module Liquid
|
||||
private
|
||||
|
||||
def tokenize
|
||||
return [] if @source.to_s.empty?
|
||||
return [] if @source.empty?
|
||||
|
||||
return @source.split("\n") if @for_liquid_tag
|
||||
|
||||
|
||||
@@ -81,5 +81,13 @@ module Liquid
|
||||
rescue ::ArgumentError
|
||||
nil
|
||||
end
|
||||
|
||||
def self.to_liquid_value(obj)
|
||||
# Enable "obj" to represent itself as a primitive value like integer, string, or boolean
|
||||
return obj.to_liquid_value if obj.respond_to?(:to_liquid_value)
|
||||
|
||||
# Otherwise return the object itself
|
||||
obj
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,6 +40,9 @@ module Liquid
|
||||
@lookups.each_index do |i|
|
||||
key = context.evaluate(@lookups[i])
|
||||
|
||||
# Cast "key" to its liquid value to enable it to act as a primitive value
|
||||
key = Liquid::Utils.to_liquid_value(key)
|
||||
|
||||
# If object is a hash- or array-like object we look for the
|
||||
# presence of the key and if its available we return it
|
||||
if object.respond_to?(:[]) &&
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
VERSION = "5.0.0"
|
||||
VERSION = "5.3.0"
|
||||
end
|
||||
|
||||
@@ -3,11 +3,19 @@
|
||||
require 'yaml'
|
||||
|
||||
module Database
|
||||
DATABASE_FILE_PATH = "#{__dir__}/vision.database.yml"
|
||||
|
||||
# Load the standard vision toolkit database and re-arrage it to be simply exportable
|
||||
# to liquid as assigns. All this is based on Shopify
|
||||
def self.tables
|
||||
@tables ||= begin
|
||||
db = YAML.load_file("#{__dir__}/vision.database.yml")
|
||||
db =
|
||||
if YAML.respond_to?(:unsafe_load_file) # Only Psych 4+ can use unsafe_load_file
|
||||
# unsafe_load_file is needed for YAML references
|
||||
YAML.unsafe_load_file(DATABASE_FILE_PATH)
|
||||
else
|
||||
YAML.load_file(DATABASE_FILE_PATH)
|
||||
end
|
||||
|
||||
# From vision source
|
||||
db['products'].each do |product|
|
||||
|
||||
@@ -24,7 +24,7 @@ class ContextSensitiveDrop < Liquid::Drop
|
||||
end
|
||||
end
|
||||
|
||||
class Category < Liquid::Drop
|
||||
class Category
|
||||
attr_accessor :name
|
||||
|
||||
def initialize(name)
|
||||
@@ -36,8 +36,9 @@ class Category < Liquid::Drop
|
||||
end
|
||||
end
|
||||
|
||||
class CategoryDrop
|
||||
class CategoryDrop < Liquid::Drop
|
||||
attr_accessor :category, :context
|
||||
|
||||
def initialize(category)
|
||||
@category = category
|
||||
end
|
||||
@@ -405,45 +406,42 @@ class ContextTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_lambda_is_called_once
|
||||
@global = 0
|
||||
|
||||
@context['callcount'] = proc {
|
||||
@global ||= 0
|
||||
@global += 1
|
||||
@global += 1
|
||||
@global.to_s
|
||||
}
|
||||
|
||||
assert_equal('1', @context['callcount'])
|
||||
assert_equal('1', @context['callcount'])
|
||||
assert_equal('1', @context['callcount'])
|
||||
|
||||
@global = nil
|
||||
end
|
||||
|
||||
def test_nested_lambda_is_called_once
|
||||
@global = 0
|
||||
|
||||
@context['callcount'] = { "lambda" => proc {
|
||||
@global ||= 0
|
||||
@global += 1
|
||||
@global += 1
|
||||
@global.to_s
|
||||
} }
|
||||
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
|
||||
@global = nil
|
||||
end
|
||||
|
||||
def test_lambda_in_array_is_called_once
|
||||
@global = 0
|
||||
|
||||
@context['callcount'] = [1, 2, proc {
|
||||
@global ||= 0
|
||||
@global += 1
|
||||
@global += 1
|
||||
@global.to_s
|
||||
}, 4, 5]
|
||||
|
||||
assert_equal('1', @context['callcount[2]'])
|
||||
assert_equal('1', @context['callcount[2]'])
|
||||
assert_equal('1', @context['callcount[2]'])
|
||||
|
||||
@global = nil
|
||||
end
|
||||
|
||||
def test_access_to_context_from_proc
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class FilterKwargTest < Minitest::Test
|
||||
module KwargFilter
|
||||
def html_tag(_tag, attributes)
|
||||
attributes
|
||||
.map { |key, value| "#{key}='#{value}'" }
|
||||
.join(' ')
|
||||
end
|
||||
end
|
||||
|
||||
include Liquid
|
||||
|
||||
def test_can_parse_data_kwargs
|
||||
with_global_filter(KwargFilter) do
|
||||
assert_equal(
|
||||
"data-src='src' data-widths='100, 200'",
|
||||
Template.parse("{{ 'img' | html_tag: data-src: 'src', data-widths: '100, 200' }}").render(nil, nil)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,27 @@
|
||||
require 'test_helper'
|
||||
|
||||
class ProfilerTest < Minitest::Test
|
||||
class TestDrop < Liquid::Drop
|
||||
def initialize(value)
|
||||
super()
|
||||
@value = value
|
||||
end
|
||||
|
||||
def to_s
|
||||
artificial_execution_time
|
||||
|
||||
@value
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Monotonic clock precision fluctuate based on the operating system
|
||||
# By introducing a small sleep we ensure ourselves to register a non zero unit of time
|
||||
def artificial_execution_time
|
||||
sleep(Process.clock_getres(Process::CLOCK_MONOTONIC))
|
||||
end
|
||||
end
|
||||
|
||||
include Liquid
|
||||
|
||||
class ProfilingFileSystem
|
||||
@@ -198,16 +219,22 @@ class ProfilerTest < Minitest::Test
|
||||
|
||||
def test_profiling_supports_self_time
|
||||
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
|
||||
t.render!("collection" => ["one", "two"])
|
||||
leaf = t.profiler[0].children[0]
|
||||
collection = [
|
||||
TestDrop.new("one"),
|
||||
TestDrop.new("two"),
|
||||
]
|
||||
output = t.render!("collection" => collection)
|
||||
assert_equal(" one two ", output)
|
||||
|
||||
assert_operator(leaf.self_time, :>, 0)
|
||||
leaf = t.profiler[0].children[0]
|
||||
assert_operator(leaf.self_time, :>, 0.0)
|
||||
end
|
||||
|
||||
def test_profiling_supports_total_time
|
||||
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
|
||||
t.render!
|
||||
t = Template.parse("{% if true %} {{ test }} {% endif %}", profile: true)
|
||||
output = t.render!("test" => TestDrop.new("one"))
|
||||
assert_equal(" one ", output)
|
||||
|
||||
assert_operator(t.profiler[0].total_time, :>, 0)
|
||||
assert_operator(t.profiler[0].total_time, :>, 0.0)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class Filters
|
||||
include Liquid::StandardFilters
|
||||
end
|
||||
|
||||
class TestThing
|
||||
attr_reader :foo
|
||||
|
||||
@@ -29,8 +25,24 @@ class TestThing
|
||||
end
|
||||
|
||||
class TestDrop < Liquid::Drop
|
||||
def test
|
||||
"testfoo"
|
||||
def initialize(value:)
|
||||
@value = value
|
||||
end
|
||||
|
||||
attr_reader :value
|
||||
|
||||
def registers
|
||||
@context.registers
|
||||
end
|
||||
end
|
||||
|
||||
class TestModel
|
||||
def initialize(value:)
|
||||
@value = value
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
TestDrop.new(value: @value)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -53,10 +65,13 @@ class NumberLikeThing < Liquid::Drop
|
||||
end
|
||||
|
||||
class StandardFiltersTest < Minitest::Test
|
||||
Filters = Class.new(Liquid::StrainerTemplate)
|
||||
Filters.add_filter(Liquid::StandardFilters)
|
||||
|
||||
include Liquid
|
||||
|
||||
def setup
|
||||
@filters = Filters.new
|
||||
@filters = Filters.new(Context.new)
|
||||
end
|
||||
|
||||
def test_size
|
||||
@@ -145,6 +160,40 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal('<strong>Hulk</strong>', @filters.escape_once('<strong>Hulk</strong>'))
|
||||
end
|
||||
|
||||
def test_base64_encode
|
||||
assert_equal('b25lIHR3byB0aHJlZQ==', @filters.base64_encode('one two three'))
|
||||
assert_equal('', @filters.base64_encode(nil))
|
||||
end
|
||||
|
||||
def test_base64_decode
|
||||
assert_equal('one two three', @filters.base64_decode('b25lIHR3byB0aHJlZQ=='))
|
||||
|
||||
exception = assert_raises(Liquid::ArgumentError) do
|
||||
@filters.base64_decode("invalidbase64")
|
||||
end
|
||||
|
||||
assert_equal('Liquid error: invalid base64 provided to base64_decode', exception.message)
|
||||
end
|
||||
|
||||
def test_base64_url_safe_encode
|
||||
assert_equal(
|
||||
'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8',
|
||||
@filters.base64_url_safe_encode('abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|')
|
||||
)
|
||||
assert_equal('', @filters.base64_url_safe_encode(nil))
|
||||
end
|
||||
|
||||
def test_base64_url_safe_decode
|
||||
assert_equal(
|
||||
'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|',
|
||||
@filters.base64_url_safe_decode('YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8')
|
||||
)
|
||||
exception = assert_raises(Liquid::ArgumentError) do
|
||||
@filters.base64_url_safe_decode("invalidbase64")
|
||||
end
|
||||
assert_equal('Liquid error: invalid base64 provided to base64_url_safe_decode', exception.message)
|
||||
end
|
||||
|
||||
def test_url_encode
|
||||
assert_equal('foo%2B1%40example.com', @filters.url_encode('[email protected]'))
|
||||
assert_equal('1', @filters.url_encode(1))
|
||||
@@ -171,13 +220,17 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal('one two three', @filters.truncatewords('one two three'))
|
||||
assert_equal(
|
||||
'Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13”...',
|
||||
@filters.truncatewords('Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13” x 16” x 10.5” high) with cover.', 15)
|
||||
@filters.truncatewords('Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13” x 16” x 10.5” high) with cover.', 15)
|
||||
)
|
||||
assert_equal("测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5))
|
||||
assert_equal('one two1', @filters.truncatewords("one two three", 2, 1))
|
||||
assert_equal('one two three...', @filters.truncatewords("one two\tthree\nfour", 3))
|
||||
assert_equal('one two...', @filters.truncatewords("one two three four", 2))
|
||||
assert_equal('one...', @filters.truncatewords("one two three four", 0))
|
||||
exception = assert_raises(Liquid::ArgumentError) do
|
||||
@filters.truncatewords("one two three four", 1 << 31)
|
||||
end
|
||||
assert_equal("Liquid error: integer #{1 << 31} too big for truncatewords", exception.message)
|
||||
end
|
||||
|
||||
def test_strip_html
|
||||
@@ -221,8 +274,8 @@ class StandardFiltersTest < Minitest::Test
|
||||
{ "price" => 1, "handle" => "gamma" },
|
||||
{ "price" => 2, "handle" => "epsilon" },
|
||||
{ "price" => 4, "handle" => "alpha" },
|
||||
{ "handle" => "delta" },
|
||||
{ "handle" => "beta" },
|
||||
{ "handle" => "delta" },
|
||||
]
|
||||
assert_equal(expectation, @filters.sort(input, "price"))
|
||||
end
|
||||
@@ -325,8 +378,9 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal(["foo"], @filters.uniq("foo"))
|
||||
assert_equal([1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1]))
|
||||
assert_equal([{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
|
||||
testdrop = TestDrop.new
|
||||
assert_equal([testdrop], @filters.uniq([testdrop, TestDrop.new], 'test'))
|
||||
test_drop = TestDrop.new(value: "test")
|
||||
test_drop_alternate = TestDrop.new(value: "test")
|
||||
assert_equal([test_drop], @filters.uniq([test_drop, test_drop_alternate], 'value'))
|
||||
end
|
||||
|
||||
def test_uniq_empty_array
|
||||
@@ -385,6 +439,16 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_template_result("woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t])
|
||||
end
|
||||
|
||||
def test_map_calls_context=
|
||||
model = TestModel.new(value: "test")
|
||||
|
||||
template = Template.parse('{{ foo | map: "registers" }}')
|
||||
template.registers[:test] = 1234
|
||||
template.assigns['foo'] = [model]
|
||||
|
||||
assert_template_result("{:test=>1234}", template.render!)
|
||||
end
|
||||
|
||||
def test_map_on_hashes
|
||||
assert_template_result("4217", '{{ thing | map: "foo" | map: "bar" }}',
|
||||
"thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] })
|
||||
@@ -403,9 +467,9 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_map_over_proc
|
||||
drop = TestDrop.new
|
||||
drop = TestDrop.new(value: "testfoo")
|
||||
p = proc { drop }
|
||||
templ = '{{ procs | map: "test" }}'
|
||||
templ = '{{ procs | map: "value" }}'
|
||||
assert_template_result("testfoo", templ, "procs" => [p])
|
||||
end
|
||||
|
||||
@@ -501,19 +565,31 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_replace
|
||||
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', '1', 2))
|
||||
assert_equal('b b b b', @filters.replace('a a a a', 'a', 'b'))
|
||||
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', 1, 2))
|
||||
assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2))
|
||||
assert_equal('1 1 1 1', @filters.replace('1 1 1 1', 2, 3))
|
||||
assert_template_result('2 2 2 2', "{{ '1 1 1 1' | replace: '1', 2 }}")
|
||||
|
||||
assert_equal('b a a a', @filters.replace_first('a a a a', 'a', 'b'))
|
||||
assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2))
|
||||
assert_equal('1 1 1 1', @filters.replace_first('1 1 1 1', 2, 3))
|
||||
assert_template_result('2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}")
|
||||
|
||||
assert_equal('a a a b', @filters.replace_last('a a a a', 'a', 'b'))
|
||||
assert_equal('1 1 1 2', @filters.replace_last('1 1 1 1', 1, 2))
|
||||
assert_equal('1 1 1 1', @filters.replace_last('1 1 1 1', 2, 3))
|
||||
assert_template_result('1 1 1 2', "{{ '1 1 1 1' | replace_last: '1', 2 }}")
|
||||
end
|
||||
|
||||
def test_remove
|
||||
assert_equal(' ', @filters.remove("a a a a", 'a'))
|
||||
assert_equal(' ', @filters.remove("1 1 1 1", 1))
|
||||
assert_equal('a a a', @filters.remove_first("a a a a", 'a '))
|
||||
assert_equal(' 1 1 1', @filters.remove_first("1 1 1 1", 1))
|
||||
assert_template_result('a a a', "{{ 'a a a a' | remove_first: 'a ' }}")
|
||||
assert_template_result(' ', "{{ '1 1 1 1' | remove: 1 }}")
|
||||
|
||||
assert_equal('b a a', @filters.remove_first("a b a a", 'a '))
|
||||
assert_template_result(' 1 1 1', "{{ '1 1 1 1' | remove_first: 1 }}")
|
||||
|
||||
assert_equal('a a b', @filters.remove_last("a a b a", ' a'))
|
||||
assert_template_result('1 1 1 ', "{{ '1 1 1 1' | remove_last: 1 }}")
|
||||
end
|
||||
|
||||
def test_pipes_in_string_arguments
|
||||
@@ -690,6 +766,8 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal("bar", @filters.default([], "bar"))
|
||||
assert_equal("bar", @filters.default({}, "bar"))
|
||||
assert_template_result('bar', "{{ false | default: 'bar' }}")
|
||||
assert_template_result('bar', "{{ drop | default: 'bar' }}", 'drop' => BooleanDrop.new(false))
|
||||
assert_template_result('Yay', "{{ drop | default: 'bar' }}", 'drop' => BooleanDrop.new(true))
|
||||
end
|
||||
|
||||
def test_default_handle_false
|
||||
@@ -700,6 +778,8 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal("bar", @filters.default([], "bar", "allow_false" => true))
|
||||
assert_equal("bar", @filters.default({}, "bar", "allow_false" => true))
|
||||
assert_template_result('false', "{{ false | default: 'bar', allow_false: true }}")
|
||||
assert_template_result('Nay', "{{ drop | default: 'bar', allow_false: true }}", 'drop' => BooleanDrop.new(false))
|
||||
assert_template_result('Yay', "{{ drop | default: 'bar', allow_false: true }}", 'drop' => BooleanDrop.new(true))
|
||||
end
|
||||
|
||||
def test_cannot_access_private_methods
|
||||
@@ -728,6 +808,18 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal(expectation, @filters.where(input, "ok"))
|
||||
end
|
||||
|
||||
def test_where_string_keys
|
||||
input = [
|
||||
"alpha", "beta", "gamma", "delta"
|
||||
]
|
||||
|
||||
expectation = [
|
||||
"beta",
|
||||
]
|
||||
|
||||
assert_equal(expectation, @filters.where(input, "be"))
|
||||
end
|
||||
|
||||
def test_where_no_key_set
|
||||
input = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
@@ -773,7 +865,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_all_filters_never_raise_non_liquid_exception
|
||||
test_drop = TestDrop.new
|
||||
test_drop = TestDrop.new(value: "test")
|
||||
test_drop.context = Context.new
|
||||
test_enum = TestEnumerable.new
|
||||
test_enum.context = Context.new
|
||||
@@ -798,19 +890,14 @@ class StandardFiltersTest < Minitest::Test
|
||||
{ 1 => "bar" },
|
||||
["foo", 123, nil, true, false, Drop, ["foo"], { foo: "bar" }],
|
||||
]
|
||||
test_types.each do |first|
|
||||
test_types.each do |other|
|
||||
(@filters.methods - Object.methods).each do |method|
|
||||
arg_count = @filters.method(method).arity
|
||||
arg_count *= -1 if arg_count < 0
|
||||
inputs = [first]
|
||||
inputs << ([other] * (arg_count - 1)) if arg_count > 1
|
||||
begin
|
||||
@filters.send(method, *inputs)
|
||||
rescue Liquid::ArgumentError, Liquid::ZeroDivisionError
|
||||
nil
|
||||
end
|
||||
end
|
||||
StandardFilters.public_instance_methods(false).each do |method|
|
||||
arg_count = @filters.method(method).arity
|
||||
arg_count *= -1 if arg_count < 0
|
||||
|
||||
test_types.repeated_permutation(arg_count) do |args|
|
||||
@filters.send(method, *args)
|
||||
rescue Liquid::Error
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -96,12 +96,12 @@ class IncludeTagTest < Minitest::Test
|
||||
|
||||
def test_include_tag_with_alias
|
||||
assert_template_result("Product: Draft 151cm ",
|
||||
"{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
"{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
end
|
||||
|
||||
def test_include_tag_for_alias
|
||||
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
|
||||
"{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
"{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
end
|
||||
|
||||
def test_include_tag_with_default_name
|
||||
|
||||
@@ -151,7 +151,7 @@ class RenderTagTest < Minitest::Test
|
||||
)
|
||||
|
||||
assert_template_result("Product: Draft 151cm ",
|
||||
"{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
"{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
end
|
||||
|
||||
def test_render_tag_with_alias
|
||||
@@ -161,7 +161,7 @@ class RenderTagTest < Minitest::Test
|
||||
)
|
||||
|
||||
assert_template_result("Product: Draft 151cm ",
|
||||
"{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
"{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
end
|
||||
|
||||
def test_render_tag_for_alias
|
||||
@@ -171,7 +171,7 @@ class RenderTagTest < Minitest::Test
|
||||
)
|
||||
|
||||
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
|
||||
"{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
"{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
end
|
||||
|
||||
def test_render_tag_for
|
||||
@@ -181,7 +181,7 @@ class RenderTagTest < Minitest::Test
|
||||
)
|
||||
|
||||
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
|
||||
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
end
|
||||
|
||||
def test_render_tag_forloop
|
||||
@@ -190,7 +190,7 @@ class RenderTagTest < Minitest::Test
|
||||
)
|
||||
|
||||
assert_template_result("Product: Draft 151cm first index:1 Product: Element 155cm last index:2 ",
|
||||
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
end
|
||||
|
||||
def test_render_tag_for_drop
|
||||
@@ -199,7 +199,7 @@ class RenderTagTest < Minitest::Test
|
||||
)
|
||||
|
||||
assert_template_result("123",
|
||||
"{% render 'loop' for loop as value %}", "loop" => TestEnumerable.new)
|
||||
"{% render 'loop' for loop as value %}", "loop" => TestEnumerable.new)
|
||||
end
|
||||
|
||||
def test_render_tag_with_drop
|
||||
@@ -208,6 +208,6 @@ class RenderTagTest < Minitest::Test
|
||||
)
|
||||
|
||||
assert_template_result("TestEnumerable",
|
||||
"{% render 'loop' with loop as value %}", "loop" => TestEnumerable.new)
|
||||
"{% render 'loop' with loop as value %}", "loop" => TestEnumerable.new)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -323,4 +323,18 @@ class TemplateTest < Minitest::Test
|
||||
result = t.render('x' => 1, 'y' => 5)
|
||||
assert_equal('12345', result)
|
||||
end
|
||||
|
||||
def test_source_string_subclass
|
||||
string_subclass = Class.new(String) do
|
||||
# E.g. ActiveSupport::SafeBuffer does this, so don't just rely on to_s to return a String
|
||||
def to_s
|
||||
self
|
||||
end
|
||||
end
|
||||
source = string_subclass.new("{% assign x = 2 -%} x= {{- x }}")
|
||||
assert_instance_of(string_subclass, source)
|
||||
output = Template.parse(source).render!
|
||||
assert_equal("x=2", output)
|
||||
assert_instance_of(String, output)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,6 +15,33 @@ class VariableTest < Minitest::Test
|
||||
assert_template_result('foobar', '{{ foo }}', 'foo' => ThingWithToLiquid.new)
|
||||
end
|
||||
|
||||
def test_variable_lookup_calls_to_liquid_value
|
||||
assert_template_result('1', '{{ foo }}', 'foo' => IntegerDrop.new('1'))
|
||||
assert_template_result('2', '{{ list[foo] }}', 'foo' => IntegerDrop.new('1'), 'list' => [1, 2, 3])
|
||||
assert_template_result('one', '{{ list[foo] }}', 'foo' => IntegerDrop.new('1'), 'list' => { 1 => 'one' })
|
||||
assert_template_result('Yay', '{{ foo }}', 'foo' => BooleanDrop.new(true))
|
||||
assert_template_result('YAY', '{{ foo | upcase }}', 'foo' => BooleanDrop.new(true))
|
||||
end
|
||||
|
||||
def test_if_tag_calls_to_liquid_value
|
||||
assert_template_result('one', '{% if foo == 1 %}one{% endif %}', 'foo' => IntegerDrop.new('1'))
|
||||
assert_template_result('one', '{% if 0 < foo %}one{% endif %}', 'foo' => IntegerDrop.new('1'))
|
||||
assert_template_result('one', '{% if foo > 0 %}one{% endif %}', 'foo' => IntegerDrop.new('1'))
|
||||
assert_template_result('true', '{% if foo == true %}true{% endif %}', 'foo' => BooleanDrop.new(true))
|
||||
assert_template_result('true', '{% if foo %}true{% endif %}', 'foo' => BooleanDrop.new(true))
|
||||
|
||||
assert_template_result('', '{% if foo %}true{% endif %}', 'foo' => BooleanDrop.new(false))
|
||||
assert_template_result('', '{% if foo == true %}True{% endif %}', 'foo' => BooleanDrop.new(false))
|
||||
end
|
||||
|
||||
def test_unless_tag_calls_to_liquid_value
|
||||
assert_template_result('', '{% unless foo %}true{% endunless %}', 'foo' => BooleanDrop.new(true))
|
||||
end
|
||||
|
||||
def test_case_tag_calls_to_liquid_value
|
||||
assert_template_result('One', '{% case foo %}{% when 1 %}One{% endcase %}', 'foo' => IntegerDrop.new('1'))
|
||||
end
|
||||
|
||||
def test_simple_with_whitespaces
|
||||
template = Template.parse(%( {{ test }} ))
|
||||
assert_equal(' worked ', template.render!('test' => 'worked'))
|
||||
@@ -104,4 +131,8 @@ class VariableTest < Minitest::Test
|
||||
def test_dynamic_find_var
|
||||
assert_template_result('bar', '{{ [key] }}', 'key' => 'foo', 'foo' => 'bar')
|
||||
end
|
||||
|
||||
def test_raw_value_variable
|
||||
assert_template_result('bar', '{{ [key] }}', 'key' => 'foo', 'foo' => 'bar')
|
||||
end
|
||||
end
|
||||
|
||||
+48
-10
@@ -72,21 +72,21 @@ module Minitest
|
||||
end
|
||||
|
||||
def with_global_filter(*globals)
|
||||
original_global_filters = Liquid::StrainerFactory.instance_variable_get(:@global_filters)
|
||||
Liquid::StrainerFactory.instance_variable_set(:@global_filters, [])
|
||||
globals.each do |global|
|
||||
Liquid::StrainerFactory.add_global_filter(global)
|
||||
end
|
||||
|
||||
Liquid::StrainerFactory.send(:strainer_class_cache).clear
|
||||
original_global_cache = Liquid::StrainerFactory::GlobalCache
|
||||
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
|
||||
Liquid::StrainerFactory.const_set(:GlobalCache, Class.new(Liquid::StrainerTemplate))
|
||||
|
||||
globals.each do |global|
|
||||
Liquid::Template.register_filter(global)
|
||||
end
|
||||
yield
|
||||
ensure
|
||||
Liquid::StrainerFactory.send(:strainer_class_cache).clear
|
||||
Liquid::StrainerFactory.instance_variable_set(:@global_filters, original_global_filters)
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
|
||||
Liquid::StrainerFactory.const_set(:GlobalCache, original_global_cache)
|
||||
Liquid::StrainerFactory.send(:strainer_class_cache).clear
|
||||
end
|
||||
end
|
||||
|
||||
def with_error_mode(mode)
|
||||
@@ -119,6 +119,44 @@ class ThingWithToLiquid
|
||||
end
|
||||
end
|
||||
|
||||
class IntegerDrop < Liquid::Drop
|
||||
def initialize(value)
|
||||
super()
|
||||
@value = value.to_i
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
@value == other
|
||||
end
|
||||
|
||||
def to_s
|
||||
@value.to_s
|
||||
end
|
||||
|
||||
def to_liquid_value
|
||||
@value
|
||||
end
|
||||
end
|
||||
|
||||
class BooleanDrop < Liquid::Drop
|
||||
def initialize(value)
|
||||
super()
|
||||
@value = value
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
@value == other
|
||||
end
|
||||
|
||||
def to_liquid_value
|
||||
@value
|
||||
end
|
||||
|
||||
def to_s
|
||||
@value ? "Yay" : "Nay"
|
||||
end
|
||||
end
|
||||
|
||||
class ErrorDrop < Liquid::Drop
|
||||
def standard_error
|
||||
raise Liquid::StandardError, 'standard error'
|
||||
|
||||
@@ -10,8 +10,8 @@ class ConditionUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_basic_condition
|
||||
assert_equal(false, Condition.new(1, '==', 2).evaluate)
|
||||
assert_equal(true, Condition.new(1, '==', 1).evaluate)
|
||||
assert_equal(false, Condition.new(1, '==', 2).evaluate(Context.new))
|
||||
assert_equal(true, Condition.new(1, '==', 1).evaluate(Context.new))
|
||||
end
|
||||
|
||||
def test_default_operators_evalute_true
|
||||
@@ -67,11 +67,11 @@ class ConditionUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_hash_compare_backwards_compatibility
|
||||
assert_nil(Condition.new({}, '>', 2).evaluate)
|
||||
assert_nil(Condition.new(2, '>', {}).evaluate)
|
||||
assert_equal(false, Condition.new({}, '==', 2).evaluate)
|
||||
assert_equal(true, Condition.new({ 'a' => 1 }, '==', 'a' => 1).evaluate)
|
||||
assert_equal(true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate)
|
||||
assert_nil(Condition.new({}, '>', 2).evaluate(Context.new))
|
||||
assert_nil(Condition.new(2, '>', {}).evaluate(Context.new))
|
||||
assert_equal(false, Condition.new({}, '==', 2).evaluate(Context.new))
|
||||
assert_equal(true, Condition.new({ 'a' => 1 }, '==', 'a' => 1).evaluate(Context.new))
|
||||
assert_equal(true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate(Context.new))
|
||||
end
|
||||
|
||||
def test_contains_works_on_arrays
|
||||
@@ -106,30 +106,29 @@ class ConditionUnitTest < Minitest::Test
|
||||
|
||||
def test_or_condition
|
||||
condition = Condition.new(1, '==', 2)
|
||||
|
||||
assert_equal(false, condition.evaluate)
|
||||
assert_equal(false, condition.evaluate(Context.new))
|
||||
|
||||
condition.or(Condition.new(2, '==', 1))
|
||||
|
||||
assert_equal(false, condition.evaluate)
|
||||
assert_equal(false, condition.evaluate(Context.new))
|
||||
|
||||
condition.or(Condition.new(1, '==', 1))
|
||||
|
||||
assert_equal(true, condition.evaluate)
|
||||
assert_equal(true, condition.evaluate(Context.new))
|
||||
end
|
||||
|
||||
def test_and_condition
|
||||
condition = Condition.new(1, '==', 1)
|
||||
|
||||
assert_equal(true, condition.evaluate)
|
||||
assert_equal(true, condition.evaluate(Context.new))
|
||||
|
||||
condition.and(Condition.new(2, '==', 2))
|
||||
|
||||
assert_equal(true, condition.evaluate)
|
||||
assert_equal(true, condition.evaluate(Context.new))
|
||||
|
||||
condition.and(Condition.new(2, '==', 1))
|
||||
|
||||
assert_equal(false, condition.evaluate)
|
||||
assert_equal(false, condition.evaluate(Context.new))
|
||||
end
|
||||
|
||||
def test_should_allow_custom_proc_operator
|
||||
@@ -148,6 +147,20 @@ class ConditionUnitTest < Minitest::Test
|
||||
assert_evaluates_true(VariableLookup.new("one"), '==', VariableLookup.new("another"))
|
||||
end
|
||||
|
||||
def test_default_context_is_deprecated
|
||||
if Gem::Version.new(Liquid::VERSION) >= Gem::Version.new('6.0.0')
|
||||
flunk("Condition#evaluate without a context argument is to be removed")
|
||||
end
|
||||
|
||||
_out, err = capture_io do
|
||||
assert_equal(true, Condition.new(1, '==', 1).evaluate)
|
||||
end
|
||||
|
||||
expected = "DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
|
||||
" and will be removed from Liquid 6.0.0."
|
||||
assert_includes(err.lines.map(&:strip), expected)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_evaluates_true(left, op, right)
|
||||
|
||||
@@ -26,6 +26,13 @@ class ParseTreeVisitorTest < Minitest::Test
|
||||
)
|
||||
end
|
||||
|
||||
def test_echo
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% echo test %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_if_condition
|
||||
assert_equal(
|
||||
["test"],
|
||||
@@ -152,6 +159,13 @@ class ParseTreeVisitorTest < Minitest::Test
|
||||
)
|
||||
end
|
||||
|
||||
def test_for_range
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% for x in (1..test) %}{% endfor %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_tablerow_in
|
||||
assert_equal(
|
||||
["test"],
|
||||
|
||||
@@ -52,7 +52,8 @@ class StrainerFactoryUnitTest < Minitest::Test
|
||||
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
|
||||
exception.message
|
||||
)
|
||||
assert_equal(exception.backtrace[0].split(':')[0], __FILE__)
|
||||
source = AccessScopeFilters.instance_method(:public_filter).source_location
|
||||
assert_equal(source.map(&:to_s), exception.backtrace[0].split(':')[0..1])
|
||||
end
|
||||
|
||||
def test_strainer_only_invokes_public_filter_methods
|
||||
|
||||
@@ -57,8 +57,8 @@ class StrainerTemplateUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
|
||||
strainer = Context.new.strainer
|
||||
with_global_filter do
|
||||
strainer = Context.new.strainer
|
||||
strainer.class.add_filter(PublicMethodOverrideFilter)
|
||||
assert(strainer.class.send(:filter_methods).include?('public_filter'))
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user