mirror of
https://github.com/Shopify/liquid.git
synced 2025-08-25 00:00:16 -04:00
Merge branch 'master' into recursive-parsing
This commit is contained in:
commit
14a17520de
@ -6,6 +6,11 @@ The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are
|
||||
## 2.6.0 / Master branch (not yet released)
|
||||
|
||||
* ...
|
||||
* Make sort filter work on enumerable drops, see #239 [Florian Weingarten, fw42]
|
||||
* Fix clashing method names in enumerable drops, see #238 [Florian Weingarten, fw42]
|
||||
* Make map filter work on enumerable drops, see #233 [Florian Weingarten, fw42]
|
||||
* Fix security issue with map filter, see #230, #232, #234, #237 [Florian Weingarten, fw42]
|
||||
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42]
|
||||
* Bugfix for #106: fix example servlet [gnowoel]
|
||||
* Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss, joliss]
|
||||
* Bugfix for #114: strip_html filter supports style tags [James Allardice, jamesallardice]
|
||||
|
@ -4,6 +4,7 @@
|
||||
* [Version history](History.md)
|
||||
* [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
|
||||
* [Liquid Wiki from Shopify](http://wiki.shopify.com/Liquid)
|
||||
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
|
||||
* [Website](http://liquidmarkup.org/)
|
||||
|
||||
## Introduction
|
||||
|
@ -44,6 +44,10 @@ module Liquid
|
||||
true
|
||||
end
|
||||
|
||||
def inspect
|
||||
self.class.to_s
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
@ -54,7 +58,16 @@ module Liquid
|
||||
|
||||
# Check for method existence without invoking respond_to?, which creates symbols
|
||||
def self.invokable?(method_name)
|
||||
@invokable_methods ||= Set.new((public_instance_methods - Liquid::Drop.public_instance_methods).map(&:to_s))
|
||||
unless @invokable_methods
|
||||
# Ruby 1.8 compatibility: call to_s on method names (which are strings in 1.8, but already symbols in 1.9)
|
||||
blacklist = (Liquid::Drop.public_instance_methods + [:each]).map(&:to_s)
|
||||
if include?(Enumerable)
|
||||
blacklist += Enumerable.public_instance_methods.map(&:to_s)
|
||||
blacklist -= [:sort, :count, :first, :min, :max, :include?].map(&:to_s)
|
||||
end
|
||||
whitelist = [:to_liquid] + (public_instance_methods.map(&:to_s) - blacklist.map(&:to_s))
|
||||
@invokable_methods = Set.new(whitelist.map(&:to_s))
|
||||
end
|
||||
@invokable_methods.include?(method_name.to_s)
|
||||
end
|
||||
end
|
||||
|
@ -81,7 +81,7 @@ module Liquid
|
||||
# Sort elements of the array
|
||||
# provide optional property with which to sort an array of hashes or drops
|
||||
def sort(input, property = nil)
|
||||
ary = [input].flatten
|
||||
ary = flatten_if_necessary(input)
|
||||
if property.nil?
|
||||
ary.sort
|
||||
elsif ary.first.respond_to?('[]') and !ary.first[property].nil?
|
||||
@ -99,11 +99,14 @@ module Liquid
|
||||
|
||||
# map/collect on a given property
|
||||
def map(input, property)
|
||||
ary = [input].flatten
|
||||
if ary.first.respond_to?('[]') and !ary.first[property].nil?
|
||||
ary.map {|e| e[property] }
|
||||
elsif ary.first.respond_to?(property)
|
||||
ary.map {|e| e.send(property) }
|
||||
flatten_if_necessary(input).map do |e|
|
||||
e = e.call if e.is_a?(Proc)
|
||||
|
||||
if property == "to_liquid"
|
||||
e
|
||||
elsif e.respond_to?(:[])
|
||||
e[property]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -244,6 +247,17 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def flatten_if_necessary(input)
|
||||
ary = if input.is_a?(Array)
|
||||
input.flatten
|
||||
elsif input.kind_of?(Enumerable)
|
||||
input
|
||||
else
|
||||
[input].flatten
|
||||
end
|
||||
ary.map{ |e| e.respond_to?(:to_liquid) ? e.to_liquid : e }
|
||||
end
|
||||
|
||||
def to_number(obj)
|
||||
case obj
|
||||
when Float
|
||||
|
@ -12,6 +12,7 @@ Gem::Specification.new do |s|
|
||||
s.authors = ["Tobias Luetke"]
|
||||
s.email = ["tobi@leetsoft.com"]
|
||||
s.homepage = "http://www.liquidmarkup.org"
|
||||
s.license = "MIT"
|
||||
#s.description = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||
|
||||
s.required_rubygems_version = ">= 1.3.7"
|
||||
|
@ -55,11 +55,44 @@ class ProductDrop < Liquid::Drop
|
||||
end
|
||||
|
||||
class EnumerableDrop < Liquid::Drop
|
||||
def before_method(method)
|
||||
method
|
||||
end
|
||||
|
||||
def size
|
||||
3
|
||||
end
|
||||
|
||||
def first
|
||||
1
|
||||
end
|
||||
|
||||
def count
|
||||
3
|
||||
end
|
||||
|
||||
def min
|
||||
1
|
||||
end
|
||||
|
||||
def max
|
||||
3
|
||||
end
|
||||
|
||||
def each
|
||||
yield 1
|
||||
yield 2
|
||||
yield 3
|
||||
end
|
||||
end
|
||||
|
||||
class RealEnumerableDrop < Liquid::Drop
|
||||
include Enumerable
|
||||
|
||||
def before_method(method)
|
||||
method
|
||||
end
|
||||
|
||||
def each
|
||||
yield 1
|
||||
yield 2
|
||||
@ -71,23 +104,34 @@ class DropsTest < Test::Unit::TestCase
|
||||
include Liquid
|
||||
|
||||
def test_product_drop
|
||||
|
||||
assert_nothing_raised do
|
||||
tpl = Liquid::Template.parse( ' ' )
|
||||
tpl.render('product' => ProductDrop.new)
|
||||
end
|
||||
end
|
||||
|
||||
def test_drop_does_only_respond_to_whitelisted_methods
|
||||
assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse("{{ product.whatever }}").render('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse('{{ product | map: "inspect" }}').render('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse('{{ product | map: "whatever" }}').render('product' => ProductDrop.new)
|
||||
end
|
||||
|
||||
def test_drops_respond_to_to_liquid
|
||||
assert_equal "text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render('product' => ProductDrop.new)
|
||||
assert_equal "text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render('product' => ProductDrop.new)
|
||||
end
|
||||
|
||||
def test_text_drop
|
||||
output = Liquid::Template.parse( ' {{ product.texts.text }} ' ).render('product' => ProductDrop.new)
|
||||
assert_equal ' text1 ', output
|
||||
|
||||
end
|
||||
|
||||
def test_unknown_method
|
||||
output = Liquid::Template.parse( ' {{ product.catchall.unknown }} ' ).render('product' => ProductDrop.new)
|
||||
assert_equal ' method: unknown ', output
|
||||
|
||||
end
|
||||
|
||||
def test_integer_argument_drop
|
||||
@ -159,6 +203,33 @@ class DropsTest < Test::Unit::TestCase
|
||||
assert_equal '3', Liquid::Template.parse( '{{collection.size}}').render('collection' => EnumerableDrop.new)
|
||||
end
|
||||
|
||||
def test_enumerable_drop_will_invoke_before_method_for_clashing_method_names
|
||||
["select", "each", "map", "cycle"].each do |method|
|
||||
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render('collection' => EnumerableDrop.new)
|
||||
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => EnumerableDrop.new)
|
||||
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render('collection' => RealEnumerableDrop.new)
|
||||
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => RealEnumerableDrop.new)
|
||||
end
|
||||
end
|
||||
|
||||
def test_some_enumerable_methods_still_get_invoked
|
||||
[ :count, :max ].each do |method|
|
||||
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => EnumerableDrop.new)
|
||||
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => EnumerableDrop.new)
|
||||
end
|
||||
|
||||
assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render('collection' => RealEnumerableDrop.new)
|
||||
|
||||
[ :min, :first ].each do |method|
|
||||
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => EnumerableDrop.new)
|
||||
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => EnumerableDrop.new)
|
||||
end
|
||||
end
|
||||
|
||||
def test_empty_string_value_access
|
||||
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render('product' => ProductDrop.new, 'value' => '')
|
||||
end
|
||||
|
@ -6,6 +6,39 @@ class Filters
|
||||
include Liquid::StandardFilters
|
||||
end
|
||||
|
||||
class TestThing
|
||||
def initialize
|
||||
@foo = 0
|
||||
end
|
||||
|
||||
def to_s
|
||||
"woot: #{@foo}"
|
||||
end
|
||||
|
||||
def [](whatever)
|
||||
to_s
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
@foo += 1
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class TestDrop < Liquid::Drop
|
||||
def test
|
||||
"testfoo"
|
||||
end
|
||||
end
|
||||
|
||||
class TestEnumerable < Liquid::Drop
|
||||
include Enumerable
|
||||
|
||||
def each(&block)
|
||||
[ { "foo" => 1, "bar" => 2 }, { "foo" => 2, "bar" => 1 }, { "foo" => 3, "bar" => 3 } ].each(&block)
|
||||
end
|
||||
end
|
||||
|
||||
class StandardFiltersTest < Test::Unit::TestCase
|
||||
include Liquid
|
||||
|
||||
@ -97,6 +130,36 @@ class StandardFiltersTest < Test::Unit::TestCase
|
||||
'ary' => [{'foo' => {'bar' => 'a'}}, {'foo' => {'bar' => 'b'}}, {'foo' => {'bar' => 'c'}}]
|
||||
end
|
||||
|
||||
def test_map_doesnt_call_arbitrary_stuff
|
||||
assert_equal "", Liquid::Template.parse('{{ "foo" | map: "__id__" }}').render
|
||||
assert_equal "", Liquid::Template.parse('{{ "foo" | map: "inspect" }}').render
|
||||
end
|
||||
|
||||
def test_map_calls_to_liquid
|
||||
t = TestThing.new
|
||||
assert_equal "woot: 1", Liquid::Template.parse('{{ foo | map: "whatever" }}').render("foo" => [t])
|
||||
end
|
||||
|
||||
def test_sort_calls_to_liquid
|
||||
t = TestThing.new
|
||||
assert_equal "woot: 1", Liquid::Template.parse('{{ foo | sort: "whatever" }}').render("foo" => [t])
|
||||
end
|
||||
|
||||
def test_map_over_proc
|
||||
drop = TestDrop.new
|
||||
p = Proc.new{ drop }
|
||||
templ = '{{ procs | map: "test" }}'
|
||||
assert_equal "testfoo", Liquid::Template.parse(templ).render("procs" => [p])
|
||||
end
|
||||
|
||||
def test_map_works_on_enumerables
|
||||
assert_equal "123", Liquid::Template.parse('{{ foo | map: "foo" }}').render!("foo" => TestEnumerable.new)
|
||||
end
|
||||
|
||||
def test_sort_works_on_enumerables
|
||||
assert_equal "213", Liquid::Template.parse('{{ foo | sort: "bar" | map: "foo" }}').render!("foo" => TestEnumerable.new)
|
||||
end
|
||||
|
||||
def test_date
|
||||
assert_equal 'May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B")
|
||||
assert_equal 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B")
|
||||
|
Loading…
x
Reference in New Issue
Block a user