liquid 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. data/CHANGELOG +38 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Manifest.txt +60 -0
  4. data/README +38 -0
  5. data/Rakefile +24 -0
  6. data/example/server/example_servlet.rb +37 -0
  7. data/example/server/liquid_servlet.rb +28 -0
  8. data/example/server/server.rb +12 -0
  9. data/example/server/templates/index.liquid +6 -0
  10. data/example/server/templates/products.liquid +45 -0
  11. data/init.rb +6 -0
  12. data/lib/extras/liquid_view.rb +27 -0
  13. data/lib/liquid.rb +66 -0
  14. data/lib/liquid/block.rb +101 -0
  15. data/lib/liquid/condition.rb +91 -0
  16. data/lib/liquid/context.rb +216 -0
  17. data/lib/liquid/document.rb +17 -0
  18. data/lib/liquid/drop.rb +48 -0
  19. data/lib/liquid/errors.rb +7 -0
  20. data/lib/liquid/extensions.rb +56 -0
  21. data/lib/liquid/file_system.rb +62 -0
  22. data/lib/liquid/htmltags.rb +64 -0
  23. data/lib/liquid/standardfilters.rb +125 -0
  24. data/lib/liquid/strainer.rb +43 -0
  25. data/lib/liquid/tag.rb +25 -0
  26. data/lib/liquid/tags/assign.rb +22 -0
  27. data/lib/liquid/tags/capture.rb +22 -0
  28. data/lib/liquid/tags/case.rb +68 -0
  29. data/lib/liquid/tags/comment.rb +9 -0
  30. data/lib/liquid/tags/cycle.rb +46 -0
  31. data/lib/liquid/tags/for.rb +81 -0
  32. data/lib/liquid/tags/if.rb +51 -0
  33. data/lib/liquid/tags/ifchanged.rb +20 -0
  34. data/lib/liquid/tags/include.rb +56 -0
  35. data/lib/liquid/tags/unless.rb +29 -0
  36. data/lib/liquid/template.rb +150 -0
  37. data/lib/liquid/variable.rb +39 -0
  38. data/test/block_test.rb +50 -0
  39. data/test/context_test.rb +340 -0
  40. data/test/drop_test.rb +139 -0
  41. data/test/error_handling_test.rb +65 -0
  42. data/test/extra/breakpoint.rb +547 -0
  43. data/test/extra/caller.rb +80 -0
  44. data/test/file_system_test.rb +30 -0
  45. data/test/filter_test.rb +98 -0
  46. data/test/helper.rb +20 -0
  47. data/test/html_tag_test.rb +24 -0
  48. data/test/if_else_test.rb +95 -0
  49. data/test/include_tag_test.rb +91 -0
  50. data/test/output_test.rb +121 -0
  51. data/test/parsing_quirks_test.rb +14 -0
  52. data/test/regexp_test.rb +39 -0
  53. data/test/security_test.rb +41 -0
  54. data/test/standard_filter_test.rb +101 -0
  55. data/test/standard_tag_test.rb +336 -0
  56. data/test/statements_test.rb +137 -0
  57. data/test/strainer_test.rb +16 -0
  58. data/test/template_test.rb +26 -0
  59. data/test/unless_else_test.rb +19 -0
  60. data/test/variable_test.rb +135 -0
  61. metadata +114 -0
@@ -0,0 +1,80 @@
1
+ class Continuation # :nodoc:
2
+ def self.create(*args, &block) # :nodoc:
3
+ cc = nil; result = callcc {|c| cc = c; block.call(cc) if block and args.empty?}
4
+ result ||= args
5
+ return *[cc, *result]
6
+ end
7
+ end
8
+
9
+ class Binding; end # for RDoc
10
+ # This method returns the binding of the method that called your
11
+ # method. It will raise an Exception when you're not inside a method.
12
+ #
13
+ # It's used like this:
14
+ # def inc_counter(amount = 1)
15
+ # Binding.of_caller do |binding|
16
+ # # Create a lambda that will increase the variable 'counter'
17
+ # # in the caller of this method when called.
18
+ # inc = eval("lambda { |arg| counter += arg }", binding)
19
+ # # We can refer to amount from inside this block safely.
20
+ # inc.call(amount)
21
+ # end
22
+ # # No other statements can go here. Put them inside the block.
23
+ # end
24
+ # counter = 0
25
+ # 2.times { inc_counter }
26
+ # counter # => 2
27
+ #
28
+ # Binding.of_caller must be the last statement in the method.
29
+ # This means that you will have to put everything you want to
30
+ # do after the call to Binding.of_caller into the block of it.
31
+ # This should be no problem however, because Ruby has closures.
32
+ # If you don't do this an Exception will be raised. Because of
33
+ # the way that Binding.of_caller is implemented it has to be
34
+ # done this way.
35
+ def Binding.of_caller(&block)
36
+ old_critical = Thread.critical
37
+ Thread.critical = true
38
+ count = 0
39
+ cc, result, error, extra_data = Continuation.create(nil, nil)
40
+ error.call if error
41
+
42
+ tracer = lambda do |*args|
43
+ type, context, extra_data = args[0], args[4], args
44
+ if type == "return"
45
+ count += 1
46
+ # First this method and then calling one will return --
47
+ # the trace event of the second event gets the context
48
+ # of the method which called the method that called this
49
+ # method.
50
+ if count == 2
51
+ # It would be nice if we could restore the trace_func
52
+ # that was set before we swapped in our own one, but
53
+ # this is impossible without overloading set_trace_func
54
+ # in current Ruby.
55
+ set_trace_func(nil)
56
+ cc.call(eval("binding", context), nil, extra_data)
57
+ end
58
+ elsif type == "line" then
59
+ nil
60
+ elsif type == "c-return" and extra_data[3] == :set_trace_func then
61
+ nil
62
+ else
63
+ set_trace_func(nil)
64
+ error_msg = "Binding.of_caller used in non-method context or " +
65
+ "trailing statements of method using it aren't in the block."
66
+ cc.call(nil, lambda { raise(ArgumentError, error_msg) }, nil)
67
+ end
68
+ end
69
+
70
+ unless result
71
+ set_trace_func(tracer)
72
+ return nil
73
+ else
74
+ Thread.critical = old_critical
75
+ case block.arity
76
+ when 1 then yield(result)
77
+ else yield(result, extra_data)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/helper'
3
+
4
+ class FileSystemTest < Test::Unit::TestCase
5
+ include Liquid
6
+
7
+ def test_default
8
+ assert_raise(FileSystemError) do
9
+ BlankFileSystem.new.read_template_file("dummy")
10
+ end
11
+ end
12
+
13
+ def test_local
14
+ file_system = Liquid::LocalFileSystem.new("/some/path")
15
+ assert_equal "/some/path/_mypartial.liquid" , file_system.full_path("mypartial")
16
+ assert_equal "/some/path/dir/_mypartial.liquid", file_system.full_path("dir/mypartial")
17
+
18
+ assert_raise(FileSystemError) do
19
+ file_system.full_path("../dir/mypartial")
20
+ end
21
+
22
+ assert_raise(FileSystemError) do
23
+ file_system.full_path("/dir/../../dir/mypartial")
24
+ end
25
+
26
+ assert_raise(FileSystemError) do
27
+ file_system.full_path("/etc/passwd")
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/helper'
3
+
4
+
5
+ module MoneyFilter
6
+ def money(input)
7
+ sprintf(' %d$ ', input)
8
+ end
9
+
10
+ def money_with_underscore(input)
11
+ sprintf(' %d$ ', input)
12
+ end
13
+ end
14
+
15
+ module CanadianMoneyFilter
16
+ def money(input)
17
+ sprintf(' %d$ CAD ', input)
18
+ end
19
+ end
20
+
21
+
22
+ class FiltersTest < Test::Unit::TestCase
23
+ include Liquid
24
+
25
+ def setup
26
+ @context = Context.new(Liquid::Template.new)
27
+ end
28
+
29
+ def test_local_filter
30
+ @context['var'] = 1000
31
+ @context.add_filters(MoneyFilter)
32
+ assert_equal ' 1000$ ', Variable.new("var | money").render(@context)
33
+ end
34
+
35
+ def test_underscore_in_filter_name
36
+ @context['var'] = 1000
37
+ @context.add_filters(MoneyFilter)
38
+ assert_equal ' 1000$ ', Variable.new("var | money_with_underscore").render(@context)
39
+ end
40
+
41
+ def test_second_filter_overwrites_first
42
+ @context['var'] = 1000
43
+ @context.add_filters(MoneyFilter)
44
+ @context.add_filters(CanadianMoneyFilter)
45
+ assert_equal ' 1000$ CAD ', Variable.new("var | money").render(@context)
46
+ end
47
+
48
+ def test_size
49
+ @context['var'] = 1000
50
+ @context.add_filters(MoneyFilter)
51
+ assert_equal 4, Variable.new("var | size").render(@context)
52
+ end
53
+
54
+ def test_join
55
+ @context['var'] = [1,2,3,4]
56
+ assert_equal "1 2 3 4", Variable.new("var | join").render(@context)
57
+ end
58
+
59
+ def test_sort
60
+ @context['value'] = 3
61
+ @context['numbers'] = [2,1,4,3]
62
+ @context['words'] = ['expected', 'as', 'alphabetic']
63
+ @context['arrays'] = [['flattened'], ['are']]
64
+ assert_equal [1,2,3,4], Variable.new("numbers | sort").render(@context)
65
+ assert_equal ['alphabetic', 'as', 'expected'],
66
+ Variable.new("words | sort").render(@context)
67
+ assert_equal [3], Variable.new("value | sort").render(@context)
68
+ assert_equal ['are', 'flattened'], Variable.new("arrays | sort").render(@context)
69
+ end
70
+
71
+ def test_strip_html
72
+ @context['var'] = "<b>bla blub</a>"
73
+ assert_equal "bla blub", Variable.new("var | strip_html").render(@context)
74
+ end
75
+
76
+ def test_capitalize
77
+ @context['var'] = "blub"
78
+ assert_equal "Blub", Variable.new("var | capitalize").render(@context)
79
+ end
80
+ end
81
+
82
+ class FiltersInTemplate < Test::Unit::TestCase
83
+ include Liquid
84
+
85
+ def test_local_global
86
+ Template.register_filter(MoneyFilter)
87
+
88
+ assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render(nil, nil)
89
+ assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, :filters => CanadianMoneyFilter)
90
+ assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, :filters => [CanadianMoneyFilter])
91
+ end
92
+
93
+ def test_local_filter_with_deprecated_syntax
94
+ assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, CanadianMoneyFilter)
95
+ assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, [CanadianMoneyFilter])
96
+ end
97
+
98
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__)+ '/extra')
3
+
4
+ require 'test/unit'
5
+ require 'test/unit/assertions'
6
+ require 'caller'
7
+ require 'breakpoint'
8
+ require File.dirname(__FILE__) + '/../lib/liquid'
9
+
10
+
11
+ module Test
12
+ module Unit
13
+ module Assertions
14
+ include Liquid
15
+ def assert_template_result(expected, template, assigns={}, message=nil)
16
+ assert_equal expected, Template.parse(template).render(assigns)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class HtmlTagTest < Test::Unit::TestCase
4
+ include Liquid
5
+
6
+ def test_html_table
7
+
8
+ assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
9
+ '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
10
+ 'numbers' => [1,2,3,4,5,6])
11
+
12
+ assert_template_result("<tr class=\"row1\">\n</tr>\n",
13
+ '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
14
+ 'numbers' => [])
15
+ end
16
+
17
+ def test_html_table_with_different_cols
18
+ assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td><td class=\"col4\"> 4 </td><td class=\"col5\"> 5 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 6 </td></tr>\n",
19
+ '{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}',
20
+ 'numbers' => [1,2,3,4,5,6])
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,95 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class IfElseTest < Test::Unit::TestCase
4
+ include Liquid
5
+
6
+ def test_if
7
+ assert_template_result(' ',' {% if false %} this text should not go into the output {% endif %} ')
8
+ assert_template_result(' this text should go into the output ',
9
+ ' {% if true %} this text should go into the output {% endif %} ')
10
+ assert_template_result(' you rock ?','{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?')
11
+ end
12
+
13
+ def test_if_else
14
+ assert_template_result(' YES ','{% if false %} NO {% else %} YES {% endif %}')
15
+ assert_template_result(' YES ','{% if true %} YES {% else %} NO {% endif %}')
16
+ assert_template_result(' YES ','{% if "foo" %} YES {% else %} NO {% endif %}')
17
+ end
18
+
19
+ def test_if_boolean
20
+ assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => true)
21
+ end
22
+
23
+ def test_hash_miss_generates_false
24
+ assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {})
25
+ end
26
+
27
+ def test_if_from_variable
28
+ assert_template_result('','{% if var %} NO {% endif %}', 'var' => false)
29
+ assert_template_result('','{% if var %} NO {% endif %}', 'var' => nil)
30
+ assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {'bar' => false})
31
+ assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {})
32
+ assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => nil)
33
+ assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => true)
34
+
35
+ assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => "text")
36
+ assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => true)
37
+ assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => 1)
38
+ assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => {})
39
+ assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => [])
40
+ assert_template_result(' YES ','{% if "foo" %} YES {% endif %}')
41
+ assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => true})
42
+ assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => "text"})
43
+ assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => 1 })
44
+ assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => {} })
45
+ assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => [] })
46
+
47
+ assert_template_result(' YES ','{% if var %} NO {% else %} YES {% endif %}', 'var' => false)
48
+ assert_template_result(' YES ','{% if var %} NO {% else %} YES {% endif %}', 'var' => nil)
49
+ assert_template_result(' YES ','{% if var %} YES {% else %} NO {% endif %}', 'var' => true)
50
+ assert_template_result(' YES ','{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text")
51
+
52
+ assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'bar' => false})
53
+ assert_template_result(' YES ','{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => true})
54
+ assert_template_result(' YES ','{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => "text"})
55
+ assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'notbar' => true})
56
+ assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {})
57
+ assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => {'bar' => true})
58
+ end
59
+
60
+ def test_nested_if
61
+ assert_template_result('', '{% if false %}{% if false %} NO {% endif %}{% endif %}')
62
+ assert_template_result('', '{% if false %}{% if true %} NO {% endif %}{% endif %}')
63
+ assert_template_result('', '{% if true %}{% if false %} NO {% endif %}{% endif %}')
64
+ assert_template_result(' YES ', '{% if true %}{% if true %} YES {% endif %}{% endif %}')
65
+
66
+ assert_template_result(' YES ', '{% if true %}{% if true %} YES {% else %} NO {% endif %}{% else %} NO {% endif %}')
67
+ assert_template_result(' YES ', '{% if true %}{% if false %} NO {% else %} YES {% endif %}{% else %} NO {% endif %}')
68
+ assert_template_result(' YES ', '{% if false %}{% if true %} NO {% else %} NONO {% endif %}{% else %} YES {% endif %}')
69
+
70
+ end
71
+
72
+ def test_comparisons_on_null
73
+ assert_template_result('','{% if null < 10 %} NO {% endif %}')
74
+ assert_template_result('','{% if null <= 10 %} NO {% endif %}')
75
+ assert_template_result('','{% if null >= 10 %} NO {% endif %}')
76
+ assert_template_result('','{% if null > 10 %} NO {% endif %}')
77
+
78
+ assert_template_result('','{% if 10 < null %} NO {% endif %}')
79
+ assert_template_result('','{% if 10 <= null %} NO {% endif %}')
80
+ assert_template_result('','{% if 10 >= null %} NO {% endif %}')
81
+ assert_template_result('','{% if 10 > null %} NO {% endif %}')
82
+ end
83
+
84
+ def test_else_if
85
+ assert_template_result('0','{% if 0 == 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}')
86
+ assert_template_result('1','{% if 0 != 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}')
87
+ assert_template_result('2','{% if 0 != 0 %}0{% elsif 1 != 1%}1{% else %}2{% endif %}')
88
+
89
+ assert_template_result('elsif','{% if false %}if{% elsif true %}elsif{% endif %}')
90
+ end
91
+
92
+ def test_syntax_error_no_variable
93
+ assert_raise(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}')}
94
+ end
95
+ end
@@ -0,0 +1,91 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class TestFileSystem
4
+ def read_template_file(template_path)
5
+ case template_path
6
+ when "product"
7
+ "Product: {{ product.title }} "
8
+
9
+ when "locale_variables"
10
+ "Locale: {{echo1}} {{echo2}}"
11
+
12
+ when "variant"
13
+ "Variant: {{ variant.title }}"
14
+
15
+ when "nested_template"
16
+ "{% include 'header' %} {% include 'body' %} {% include 'footer' %}"
17
+
18
+ when "body"
19
+ "body {% include 'body_detail' %}"
20
+
21
+ when "nested_product_template"
22
+ "Product: {{ nested_product_template.title }} {%include 'details'%} "
23
+
24
+ else
25
+ template_path
26
+ end
27
+ end
28
+ end
29
+
30
+ class IncludeTagTest < Test::Unit::TestCase
31
+ include Liquid
32
+
33
+ def setup
34
+ Liquid::Template.file_system = TestFileSystem.new
35
+ end
36
+
37
+
38
+ def test_include_tag_with
39
+ assert_equal "Product: Draft 151cm ",
40
+ Template.parse("{% include 'product' with products[0] %}").render( "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] )
41
+ end
42
+
43
+ def test_include_tag_for
44
+
45
+ assert_equal "Product: Draft 151cm Product: Element 155cm ",
46
+ Template.parse("{% include 'product' for products %}").render( "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] )
47
+ end
48
+
49
+ def test_include_tag_with_local_variables
50
+ assert_equal "Locale: test123 ",
51
+ Template.parse("{% include 'locale_variables' echo1: 'test123' %}").render
52
+ end
53
+
54
+ def test_include_tag_with_multiple_local_variables
55
+ assert_equal "Locale: test123 test321",
56
+ Template.parse("{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}").render
57
+ end
58
+
59
+ def test_include_tag_with_multiple_local_variables_from_context
60
+ assert_equal "Locale: test123 test321",
61
+ Template.parse("{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}").render('echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'})
62
+ end
63
+
64
+ def test_nested_include_tag
65
+ assert_equal "body body_detail",
66
+ Template.parse("{% include 'body' %}").render
67
+
68
+ assert_equal "header body body_detail footer",
69
+ Template.parse("{% include 'nested_template' %}").render
70
+ end
71
+
72
+ def test_nested_include_with_variable
73
+
74
+ assert_equal "Product: Draft 151cm details ",
75
+ Template.parse("{% include 'nested_product_template' with product %}").render("product" => {"title" => 'Draft 151cm'})
76
+
77
+ assert_equal "Product: Draft 151cm details Product: Element 155cm details ",
78
+ Template.parse("{% include 'nested_product_template' for products %}").render("products" => [{"title" => 'Draft 151cm'}, {"title" => 'Element 155cm'}])
79
+
80
+ end
81
+
82
+ def test_dynamically_choosen_template
83
+
84
+ assert_equal "Test123", Template.parse("{% include template %}").render("template" => 'Test123')
85
+ assert_equal "Test321", Template.parse("{% include template %}").render("template" => 'Test321')
86
+
87
+ assert_equal "Product: Draft 151cm ", Template.parse("{% include template for product %}").render("template" => 'product', 'product' => { 'title' => 'Draft 151cm'})
88
+
89
+ end
90
+
91
+ end