mirror of
https://github.com/kemko/liquid.git
synced 2026-01-06 18:25:41 +03:00
Compare commits
18 Commits
v3.0.1
...
3-0-stable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d19fdde7f | ||
|
|
e934826820 | ||
|
|
529cc60ef1 | ||
|
|
5068fcae03 | ||
|
|
4095bfa9f8 | ||
|
|
0148289b88 | ||
|
|
d891c2b7ab | ||
|
|
ebbfb54de4 | ||
|
|
8f84ddb5ce | ||
|
|
09de50dcb1 | ||
|
|
49f2af4209 | ||
|
|
5d7c00a202 | ||
|
|
9bd05110dc | ||
|
|
9dd24824f9 | ||
|
|
291b58bc91 | ||
|
|
8c193e203f | ||
|
|
47dbcd93a5 | ||
|
|
000d0c911b |
34
History.md
34
History.md
@@ -1,8 +1,28 @@
|
|||||||
# Liquid Version History
|
# Liquid Version History
|
||||||
|
|
||||||
## 3.0.0 / not yet released / branch "master"
|
## 3.0.5 / 2015-07-23 / branch "3-0-stable"
|
||||||
|
|
||||||
|
* Fix test failure under certain timezones [Dylan Thacker-Smith]
|
||||||
|
|
||||||
|
## 3.0.4 / 2015-07-17
|
||||||
|
|
||||||
|
* Fix chained access to multi-dimensional hashes [Florian Weingarten]
|
||||||
|
|
||||||
|
## 3.0.3 / 2015-05-28
|
||||||
|
|
||||||
|
* Fix condition parse order in strict mode (#569) [Justin Li, pushrax]
|
||||||
|
|
||||||
|
## 3.0.2 / 2015-04-24
|
||||||
|
|
||||||
|
* Expose VariableLookup private members (#551) [Justin Li, pushrax]
|
||||||
|
* Documentation fixes
|
||||||
|
|
||||||
|
## 3.0.1 / 2015-01-23
|
||||||
|
|
||||||
|
* Remove duplicate `index0` key in TableRow tag (#502) [Alfred Xing]
|
||||||
|
|
||||||
|
## 3.0.0 / 2014-11-12
|
||||||
|
|
||||||
* ...
|
|
||||||
* Removed Block#end_tag. Instead, override parse with `super` followed by your code. See #446 [Dylan Thacker-Smith, dylanahsmith]
|
* Removed Block#end_tag. Instead, override parse with `super` followed by your code. See #446 [Dylan Thacker-Smith, dylanahsmith]
|
||||||
* Fixed condition with wrong data types, see #423 [Bogdan Gusiev]
|
* Fixed condition with wrong data types, see #423 [Bogdan Gusiev]
|
||||||
* Add url_encode to standard filters, see #421 [Derrick Reimer, djreimer]
|
* Add url_encode to standard filters, see #421 [Derrick Reimer, djreimer]
|
||||||
@@ -36,7 +56,15 @@
|
|||||||
* Make map filter work on enumerable drops, see #233 [Florian Weingarten, fw42]
|
* Make map filter work on enumerable drops, see #233 [Florian Weingarten, fw42]
|
||||||
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42]
|
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42]
|
||||||
|
|
||||||
## 2.6.1 / 2014-01-10 / branch "2-6-stable"
|
## 2.6.3 / 2015-07-23 / branch "2-6-stable"
|
||||||
|
|
||||||
|
* Fix test failure under certain timezones [Dylan Thacker-Smith]
|
||||||
|
|
||||||
|
## 2.6.2 / 2015-01-23
|
||||||
|
|
||||||
|
* Remove duplicate hash key [Parker Moore]
|
||||||
|
|
||||||
|
## 2.6.1 / 2014-01-10
|
||||||
|
|
||||||
Security fix, cherry-picked from master (4e14a65):
|
Security fix, cherry-picked from master (4e14a65):
|
||||||
* Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk]
|
* Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk]
|
||||||
|
|||||||
@@ -61,21 +61,8 @@ module Liquid
|
|||||||
# for that
|
# for that
|
||||||
def add_filters(filters)
|
def add_filters(filters)
|
||||||
filters = [filters].flatten.compact
|
filters = [filters].flatten.compact
|
||||||
filters.each do |f|
|
@filters += filters
|
||||||
raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
|
@strainer = nil
|
||||||
Strainer.add_known_filter(f)
|
|
||||||
end
|
|
||||||
|
|
||||||
# If strainer is already setup then there's no choice but to use a runtime
|
|
||||||
# extend call. If strainer is not yet created, we can utilize strainers
|
|
||||||
# cached class based API, which avoids busting the method cache.
|
|
||||||
if @strainer
|
|
||||||
filters.each do |f|
|
|
||||||
strainer.extend(f)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
@filters.concat filters
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# are there any not handled interrupts?
|
# are there any not handled interrupts?
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ module Liquid
|
|||||||
|
|
||||||
def variable_signature
|
def variable_signature
|
||||||
str = consume(:id)
|
str = consume(:id)
|
||||||
if look(:open_square)
|
while look(:open_square)
|
||||||
str << consume
|
str << consume
|
||||||
str << expression
|
str << expression
|
||||||
str << consume(:close_square)
|
str << consume(:close_square)
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ module Liquid
|
|||||||
else
|
else
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
rescue ArgumentError
|
rescue ::ArgumentError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ module Liquid
|
|||||||
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
|
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
|
||||||
# Context#add_filters or Template.register_filter
|
# Context#add_filters or Template.register_filter
|
||||||
class Strainer #:nodoc:
|
class Strainer #:nodoc:
|
||||||
@@filters = []
|
@@global_strainer = Class.new(Strainer) do
|
||||||
@@known_filters = Set.new
|
@filter_methods = Set.new
|
||||||
@@known_methods = Set.new
|
end
|
||||||
@@strainer_class_cache = Hash.new do |hash, filters|
|
@@strainer_class_cache = Hash.new do |hash, filters|
|
||||||
hash[filters] = Class.new(Strainer) do
|
hash[filters] = Class.new(@@global_strainer) do
|
||||||
filters.each { |f| include f }
|
@filter_methods = @@global_strainer.filter_methods.dup
|
||||||
|
filters.each { |f| add_filter(f) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -21,33 +22,32 @@ module Liquid
|
|||||||
@context = context
|
@context = context
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.global_filter(filter)
|
def self.filter_methods
|
||||||
raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module)
|
@filter_methods
|
||||||
add_known_filter(filter)
|
|
||||||
@@filters << filter unless @@filters.include?(filter)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.add_known_filter(filter)
|
def self.add_filter(filter)
|
||||||
unless @@known_filters.include?(filter)
|
raise ArgumentError, "Expected module but got: #{f.class}" unless filter.is_a?(Module)
|
||||||
@@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s))
|
unless self.class.include?(filter)
|
||||||
new_methods = filter.instance_methods.map(&:to_s)
|
self.send(:include, filter)
|
||||||
new_methods.reject!{ |m| @@method_blacklist.include?(m) }
|
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
||||||
@@known_methods.merge(new_methods)
|
|
||||||
@@known_filters.add(filter)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.strainer_class_cache
|
def self.global_filter(filter)
|
||||||
@@strainer_class_cache
|
@@global_strainer.add_filter(filter)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.invokable?(method)
|
||||||
|
@filter_methods.include?(method.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.create(context, filters = [])
|
def self.create(context, filters = [])
|
||||||
filters = @@filters + filters
|
@@strainer_class_cache[filters].new(context)
|
||||||
strainer_class_cache[filters].new(context)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def invoke(method, *args)
|
def invoke(method, *args)
|
||||||
if invokable?(method)
|
if self.class.invokable?(method)
|
||||||
send(method, *args)
|
send(method, *args)
|
||||||
else
|
else
|
||||||
args.first
|
args.first
|
||||||
@@ -55,9 +55,5 @@ module Liquid
|
|||||||
rescue ::ArgumentError => e
|
rescue ::ArgumentError => e
|
||||||
raise Liquid::ArgumentError.new(e.message)
|
raise Liquid::ArgumentError.new(e.message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def invokable?(method)
|
|
||||||
@@known_methods.include?(method.to_s) && respond_to?(method)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -78,16 +78,16 @@ module Liquid
|
|||||||
|
|
||||||
def strict_parse(markup)
|
def strict_parse(markup)
|
||||||
p = Parser.new(markup)
|
p = Parser.new(markup)
|
||||||
|
condition = parse_binary_comparison(p)
|
||||||
condition = parse_comparison(p)
|
|
||||||
|
|
||||||
while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
|
|
||||||
new_cond = parse_comparison(p)
|
|
||||||
new_cond.send(op, condition)
|
|
||||||
condition = new_cond
|
|
||||||
end
|
|
||||||
p.consume(:end_of_string)
|
p.consume(:end_of_string)
|
||||||
|
condition
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_binary_comparison(p)
|
||||||
|
condition = parse_comparison(p)
|
||||||
|
if op = (p.id?('and'.freeze) || p.id?('or'.freeze))
|
||||||
|
condition.send(op, parse_binary_comparison(p))
|
||||||
|
end
|
||||||
condition
|
condition
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ require File.dirname(__FILE__) + '/if'
|
|||||||
module Liquid
|
module Liquid
|
||||||
# Unless is a conditional just like 'if' but works on the inverse logic.
|
# Unless is a conditional just like 'if' but works on the inverse logic.
|
||||||
#
|
#
|
||||||
# {% unless x < 0 %} x is greater than zero {% end %}
|
# {% unless x < 0 %} x is greater than zero {% endunless %}
|
||||||
#
|
#
|
||||||
class Unless < If
|
class Unless < If
|
||||||
def render(context)
|
def render(context)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ module Liquid
|
|||||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
||||||
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
|
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
|
||||||
|
|
||||||
|
attr_reader :name, :lookups
|
||||||
|
|
||||||
def self.parse(markup)
|
def self.parse(markup)
|
||||||
new(markup)
|
new(markup)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
module Liquid
|
module Liquid
|
||||||
VERSION = "3.0.1"
|
VERSION = "3.0.6"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ end
|
|||||||
class FiltersTest < Minitest::Test
|
class FiltersTest < Minitest::Test
|
||||||
include Liquid
|
include Liquid
|
||||||
|
|
||||||
|
module OverrideObjectMethodFilter
|
||||||
|
def tap(input)
|
||||||
|
"tap overridden"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def setup
|
def setup
|
||||||
@context = Context.new
|
@context = Context.new
|
||||||
end
|
end
|
||||||
@@ -105,6 +111,13 @@ class FiltersTest < Minitest::Test
|
|||||||
output = Variable.new(%! 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' !).render(@context)
|
output = Variable.new(%! 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' !).render(@context)
|
||||||
assert_equal 'hello john, doe', output
|
assert_equal 'hello john, doe', output
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_override_object_method_in_filter
|
||||||
|
assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, :filters => [OverrideObjectMethodFilter])
|
||||||
|
|
||||||
|
# tap still treated as a non-existent filter
|
||||||
|
assert_equal "1000", Template.parse("{{var | tap}}").render!({ 'var' => 1000 })
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class FiltersInTemplate < Minitest::Test
|
class FiltersInTemplate < Minitest::Test
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ class OutputTest < Minitest::Test
|
|||||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_variable_traversing_with_two_brackets
|
||||||
|
text = %({{ site.data.menu[include.menu][include.locale] }})
|
||||||
|
assert_equal "it works!", Template.parse(text).render!(
|
||||||
|
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
|
||||||
|
"include" => { "menu" => "foo", "locale" => "bar" }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def test_variable_traversing
|
def test_variable_traversing
|
||||||
text = %| {{car.bmw}} {{car.gm}} {{car.bmw}} |
|
text = %| {{car.bmw}} {{car.gm}} {{car.bmw}} |
|
||||||
|
|
||||||
|
|||||||
@@ -252,8 +252,10 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
|
|
||||||
assert_equal nil, @filters.date(nil, "%B")
|
assert_equal nil, @filters.date(nil, "%B")
|
||||||
|
|
||||||
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
|
with_timezone("UTC") do
|
||||||
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
|
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
|
||||||
|
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_first_last
|
def test_first_last
|
||||||
@@ -376,4 +378,19 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
def test_cannot_access_private_methods
|
def test_cannot_access_private_methods
|
||||||
assert_template_result('a',"{{ 'a' | to_number }}")
|
assert_template_result('a',"{{ 'a' | to_number }}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_date_raises_nothing
|
||||||
|
assert_template_result('', "{{ '' | date: '%D' }}")
|
||||||
|
assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def with_timezone(tz)
|
||||||
|
old_tz = ENV['TZ']
|
||||||
|
ENV['TZ'] = tz
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
ENV['TZ'] = old_tz
|
||||||
|
end
|
||||||
end # StandardFiltersTest
|
end # StandardFiltersTest
|
||||||
|
|||||||
@@ -166,4 +166,25 @@ class IfElseTagTest < Minitest::Test
|
|||||||
assert_template_result('', %({% if 1 or throw or or 1 %}yes{% endif %}))
|
assert_template_result('', %({% if 1 or throw or or 1 %}yes{% endif %}))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_multiple_conditions
|
||||||
|
tpl = "{% if a or b and c %}true{% else %}false{% endif %}"
|
||||||
|
|
||||||
|
tests = {
|
||||||
|
[true, true, true] => true,
|
||||||
|
[true, true, false] => true,
|
||||||
|
[true, false, true] => true,
|
||||||
|
[true, false, false] => true,
|
||||||
|
[false, true, true] => true,
|
||||||
|
[false, true, false] => false,
|
||||||
|
[false, false, true] => false,
|
||||||
|
[false, false, false] => false,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests.each do |vals, expected|
|
||||||
|
a, b, c = vals
|
||||||
|
assigns = { 'a' => a, 'b' => b, 'c' => c }
|
||||||
|
assert_template_result expected.to_s, tpl, assigns, assigns.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env ruby
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
ENV["MT_NO_EXPECTATIONS"] = "1"
|
||||||
require 'minitest/autorun'
|
require 'minitest/autorun'
|
||||||
require 'spy/integration'
|
require 'spy/integration'
|
||||||
|
|
||||||
@@ -31,13 +32,13 @@ module Minitest
|
|||||||
include Liquid
|
include Liquid
|
||||||
|
|
||||||
def assert_template_result(expected, template, assigns = {}, message = nil)
|
def assert_template_result(expected, template, assigns = {}, message = nil)
|
||||||
assert_equal expected, Template.parse(template).render!(assigns)
|
assert_equal expected, Template.parse(template).render!(assigns), message
|
||||||
end
|
end
|
||||||
|
|
||||||
def assert_template_result_matches(expected, template, assigns = {}, message = nil)
|
def assert_template_result_matches(expected, template, assigns = {}, message = nil)
|
||||||
return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp
|
return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp
|
||||||
|
|
||||||
assert_match expected, Template.parse(template).render!(assigns)
|
assert_match expected, Template.parse(template).render!(assigns), message
|
||||||
end
|
end
|
||||||
|
|
||||||
def assert_match_syntax_error(match, template, registers = {})
|
def assert_match_syntax_error(match, template, registers = {})
|
||||||
@@ -48,13 +49,19 @@ module Minitest
|
|||||||
end
|
end
|
||||||
|
|
||||||
def with_global_filter(*globals)
|
def with_global_filter(*globals)
|
||||||
original_filters = Array.new(Liquid::Strainer.class_variable_get(:@@filters))
|
original_global_strainer = Liquid::Strainer.class_variable_get(:@@global_strainer)
|
||||||
|
Liquid::Strainer.class_variable_set(:@@global_strainer, Class.new(Liquid::Strainer) do
|
||||||
|
@filter_methods = Set.new
|
||||||
|
end)
|
||||||
|
Liquid::Strainer.class_variable_get(:@@strainer_class_cache).clear
|
||||||
|
|
||||||
globals.each do |global|
|
globals.each do |global|
|
||||||
Liquid::Template.register_filter(global)
|
Liquid::Template.register_filter(global)
|
||||||
end
|
end
|
||||||
yield
|
yield
|
||||||
ensure
|
ensure
|
||||||
Liquid::Strainer.class_variable_set(:@@filters, original_filters)
|
Liquid::Strainer.class_variable_get(:@@strainer_class_cache).clear
|
||||||
|
Liquid::Strainer.class_variable_set(:@@global_strainer, original_global_strainer)
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_taint_mode(mode)
|
def with_taint_mode(mode)
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ class StrainerUnitTest < Minitest::Test
|
|||||||
|
|
||||||
def test_strainer_only_invokes_public_filter_methods
|
def test_strainer_only_invokes_public_filter_methods
|
||||||
strainer = Strainer.create(nil)
|
strainer = Strainer.create(nil)
|
||||||
assert_equal false, strainer.invokable?('__test__')
|
assert_equal false, strainer.class.invokable?('__test__')
|
||||||
assert_equal false, strainer.invokable?('test')
|
assert_equal false, strainer.class.invokable?('test')
|
||||||
assert_equal false, strainer.invokable?('instance_eval')
|
assert_equal false, strainer.class.invokable?('instance_eval')
|
||||||
assert_equal false, strainer.invokable?('__send__')
|
assert_equal false, strainer.class.invokable?('__send__')
|
||||||
assert_equal true, strainer.invokable?('size') # from the standard lib
|
assert_equal true, strainer.class.invokable?('size') # from the standard lib
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_strainer_returns_nil_if_no_filter_method_found
|
def test_strainer_returns_nil_if_no_filter_method_found
|
||||||
@@ -63,9 +63,7 @@ class StrainerUnitTest < Minitest::Test
|
|||||||
assert_kind_of Strainer, strainer
|
assert_kind_of Strainer, strainer
|
||||||
assert_kind_of a, strainer
|
assert_kind_of a, strainer
|
||||||
assert_kind_of b, strainer
|
assert_kind_of b, strainer
|
||||||
Strainer.class_variable_get(:@@filters).each do |m|
|
assert_kind_of Liquid::StandardFilters, strainer
|
||||||
assert_kind_of m, strainer
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end # StrainerTest
|
end # StrainerTest
|
||||||
|
|||||||
@@ -136,4 +136,10 @@ class VariableUnitTest < Minitest::Test
|
|||||||
var = Variable.new(%! name_of_variable | upcase !)
|
var = Variable.new(%! name_of_variable | upcase !)
|
||||||
assert_equal " name_of_variable | upcase ", var.raw
|
assert_equal " name_of_variable | upcase ", var.raw
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_variable_lookup_interface
|
||||||
|
lookup = VariableLookup.new('a.b.c')
|
||||||
|
assert_equal 'a', lookup.name
|
||||||
|
assert_equal ['b', 'c'], lookup.lookups
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user