liquid-4-0-2 4.0.2

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 (103) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +235 -0
  3. data/LICENSE +20 -0
  4. data/README.md +108 -0
  5. data/lib/liquid.rb +80 -0
  6. data/lib/liquid/block.rb +77 -0
  7. data/lib/liquid/block_body.rb +142 -0
  8. data/lib/liquid/condition.rb +151 -0
  9. data/lib/liquid/context.rb +226 -0
  10. data/lib/liquid/document.rb +27 -0
  11. data/lib/liquid/drop.rb +78 -0
  12. data/lib/liquid/errors.rb +56 -0
  13. data/lib/liquid/expression.rb +49 -0
  14. data/lib/liquid/extensions.rb +74 -0
  15. data/lib/liquid/file_system.rb +73 -0
  16. data/lib/liquid/forloop_drop.rb +42 -0
  17. data/lib/liquid/i18n.rb +39 -0
  18. data/lib/liquid/interrupts.rb +16 -0
  19. data/lib/liquid/lexer.rb +55 -0
  20. data/lib/liquid/locales/en.yml +26 -0
  21. data/lib/liquid/parse_context.rb +38 -0
  22. data/lib/liquid/parse_tree_visitor.rb +42 -0
  23. data/lib/liquid/parser.rb +90 -0
  24. data/lib/liquid/parser_switching.rb +31 -0
  25. data/lib/liquid/profiler.rb +158 -0
  26. data/lib/liquid/profiler/hooks.rb +23 -0
  27. data/lib/liquid/range_lookup.rb +37 -0
  28. data/lib/liquid/resource_limits.rb +23 -0
  29. data/lib/liquid/standardfilters.rb +485 -0
  30. data/lib/liquid/strainer.rb +66 -0
  31. data/lib/liquid/tablerowloop_drop.rb +62 -0
  32. data/lib/liquid/tag.rb +43 -0
  33. data/lib/liquid/tags/assign.rb +59 -0
  34. data/lib/liquid/tags/break.rb +18 -0
  35. data/lib/liquid/tags/capture.rb +38 -0
  36. data/lib/liquid/tags/case.rb +94 -0
  37. data/lib/liquid/tags/comment.rb +16 -0
  38. data/lib/liquid/tags/continue.rb +18 -0
  39. data/lib/liquid/tags/cycle.rb +65 -0
  40. data/lib/liquid/tags/decrement.rb +35 -0
  41. data/lib/liquid/tags/for.rb +203 -0
  42. data/lib/liquid/tags/if.rb +122 -0
  43. data/lib/liquid/tags/ifchanged.rb +18 -0
  44. data/lib/liquid/tags/include.rb +124 -0
  45. data/lib/liquid/tags/increment.rb +31 -0
  46. data/lib/liquid/tags/raw.rb +47 -0
  47. data/lib/liquid/tags/table_row.rb +62 -0
  48. data/lib/liquid/tags/unless.rb +30 -0
  49. data/lib/liquid/template.rb +254 -0
  50. data/lib/liquid/tokenizer.rb +31 -0
  51. data/lib/liquid/utils.rb +83 -0
  52. data/lib/liquid/variable.rb +148 -0
  53. data/lib/liquid/variable_lookup.rb +88 -0
  54. data/lib/liquid/version.rb +4 -0
  55. data/test/fixtures/en_locale.yml +9 -0
  56. data/test/integration/assign_test.rb +48 -0
  57. data/test/integration/blank_test.rb +106 -0
  58. data/test/integration/block_test.rb +12 -0
  59. data/test/integration/capture_test.rb +50 -0
  60. data/test/integration/context_test.rb +32 -0
  61. data/test/integration/document_test.rb +19 -0
  62. data/test/integration/drop_test.rb +273 -0
  63. data/test/integration/error_handling_test.rb +260 -0
  64. data/test/integration/filter_test.rb +178 -0
  65. data/test/integration/hash_ordering_test.rb +23 -0
  66. data/test/integration/output_test.rb +123 -0
  67. data/test/integration/parse_tree_visitor_test.rb +247 -0
  68. data/test/integration/parsing_quirks_test.rb +122 -0
  69. data/test/integration/render_profiling_test.rb +154 -0
  70. data/test/integration/security_test.rb +80 -0
  71. data/test/integration/standard_filter_test.rb +698 -0
  72. data/test/integration/tags/break_tag_test.rb +15 -0
  73. data/test/integration/tags/continue_tag_test.rb +15 -0
  74. data/test/integration/tags/for_tag_test.rb +410 -0
  75. data/test/integration/tags/if_else_tag_test.rb +188 -0
  76. data/test/integration/tags/include_tag_test.rb +245 -0
  77. data/test/integration/tags/increment_tag_test.rb +23 -0
  78. data/test/integration/tags/raw_tag_test.rb +31 -0
  79. data/test/integration/tags/standard_tag_test.rb +296 -0
  80. data/test/integration/tags/statements_test.rb +111 -0
  81. data/test/integration/tags/table_row_test.rb +64 -0
  82. data/test/integration/tags/unless_else_tag_test.rb +26 -0
  83. data/test/integration/template_test.rb +332 -0
  84. data/test/integration/trim_mode_test.rb +529 -0
  85. data/test/integration/variable_test.rb +96 -0
  86. data/test/test_helper.rb +116 -0
  87. data/test/unit/block_unit_test.rb +58 -0
  88. data/test/unit/condition_unit_test.rb +166 -0
  89. data/test/unit/context_unit_test.rb +489 -0
  90. data/test/unit/file_system_unit_test.rb +35 -0
  91. data/test/unit/i18n_unit_test.rb +37 -0
  92. data/test/unit/lexer_unit_test.rb +51 -0
  93. data/test/unit/parser_unit_test.rb +82 -0
  94. data/test/unit/regexp_unit_test.rb +44 -0
  95. data/test/unit/strainer_unit_test.rb +164 -0
  96. data/test/unit/tag_unit_test.rb +21 -0
  97. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  98. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  99. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  100. data/test/unit/template_unit_test.rb +78 -0
  101. data/test/unit/tokenizer_unit_test.rb +55 -0
  102. data/test/unit/variable_unit_test.rb +162 -0
  103. metadata +224 -0
@@ -0,0 +1,96 @@
1
+ require 'test_helper'
2
+
3
+ class VariableTest < Minitest::Test
4
+ include Liquid
5
+
6
+ def test_simple_variable
7
+ template = Template.parse(%({{test}}))
8
+ assert_equal 'worked', template.render!('test' => 'worked')
9
+ assert_equal 'worked wonderfully', template.render!('test' => 'worked wonderfully')
10
+ end
11
+
12
+ def test_variable_render_calls_to_liquid
13
+ assert_template_result 'foobar', '{{ foo }}', 'foo' => ThingWithToLiquid.new
14
+ end
15
+
16
+ def test_simple_with_whitespaces
17
+ template = Template.parse(%( {{ test }} ))
18
+ assert_equal ' worked ', template.render!('test' => 'worked')
19
+ assert_equal ' worked wonderfully ', template.render!('test' => 'worked wonderfully')
20
+ end
21
+
22
+ def test_ignore_unknown
23
+ template = Template.parse(%({{ test }}))
24
+ assert_equal '', template.render!
25
+ end
26
+
27
+ def test_using_blank_as_variable_name
28
+ template = Template.parse("{% assign foo = blank %}{{ foo }}")
29
+ assert_equal '', template.render!
30
+ end
31
+
32
+ def test_using_empty_as_variable_name
33
+ template = Template.parse("{% assign foo = empty %}{{ foo }}")
34
+ assert_equal '', template.render!
35
+ end
36
+
37
+ def test_hash_scoping
38
+ template = Template.parse(%({{ test.test }}))
39
+ assert_equal 'worked', template.render!('test' => { 'test' => 'worked' })
40
+ end
41
+
42
+ def test_false_renders_as_false
43
+ assert_equal 'false', Template.parse("{{ foo }}").render!('foo' => false)
44
+ assert_equal 'false', Template.parse("{{ false }}").render!
45
+ end
46
+
47
+ def test_nil_renders_as_empty_string
48
+ assert_equal '', Template.parse("{{ nil }}").render!
49
+ assert_equal 'cat', Template.parse("{{ nil | append: 'cat' }}").render!
50
+ end
51
+
52
+ def test_preset_assigns
53
+ template = Template.parse(%({{ test }}))
54
+ template.assigns['test'] = 'worked'
55
+ assert_equal 'worked', template.render!
56
+ end
57
+
58
+ def test_reuse_parsed_template
59
+ template = Template.parse(%({{ greeting }} {{ name }}))
60
+ template.assigns['greeting'] = 'Goodbye'
61
+ assert_equal 'Hello Tobi', template.render!('greeting' => 'Hello', 'name' => 'Tobi')
62
+ assert_equal 'Hello ', template.render!('greeting' => 'Hello', 'unknown' => 'Tobi')
63
+ assert_equal 'Hello Brian', template.render!('greeting' => 'Hello', 'name' => 'Brian')
64
+ assert_equal 'Goodbye Brian', template.render!('name' => 'Brian')
65
+ assert_equal({ 'greeting' => 'Goodbye' }, template.assigns)
66
+ end
67
+
68
+ def test_assigns_not_polluted_from_template
69
+ template = Template.parse(%({{ test }}{% assign test = 'bar' %}{{ test }}))
70
+ template.assigns['test'] = 'baz'
71
+ assert_equal 'bazbar', template.render!
72
+ assert_equal 'bazbar', template.render!
73
+ assert_equal 'foobar', template.render!('test' => 'foo')
74
+ assert_equal 'bazbar', template.render!
75
+ end
76
+
77
+ def test_hash_with_default_proc
78
+ template = Template.parse(%(Hello {{ test }}))
79
+ assigns = Hash.new { |h, k| raise "Unknown variable '#{k}'" }
80
+ assigns['test'] = 'Tobi'
81
+ assert_equal 'Hello Tobi', template.render!(assigns)
82
+ assigns.delete('test')
83
+ e = assert_raises(RuntimeError) do
84
+ template.render!(assigns)
85
+ end
86
+ assert_equal "Unknown variable 'test'", e.message
87
+ end
88
+
89
+ def test_multiline_variable
90
+ assert_equal 'worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked')
91
+ end
92
+
93
+ def test_render_symbol
94
+ assert_template_result 'bar', '{{ foo }}', 'foo' => :bar
95
+ end
96
+ end
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ENV["MT_NO_EXPECTATIONS"] = "1"
4
+ require 'minitest/autorun'
5
+
6
+ $LOAD_PATH.unshift(File.join(File.expand_path(__dir__), '..', 'lib'))
7
+ require 'liquid.rb'
8
+ require 'liquid/profiler'
9
+
10
+ mode = :strict
11
+ if env_mode = ENV['LIQUID_PARSER_MODE']
12
+ puts "-- #{env_mode.upcase} ERROR MODE"
13
+ mode = env_mode.to_sym
14
+ end
15
+ Liquid::Template.error_mode = mode
16
+
17
+ if ENV['LIQUID-C'] == '1'
18
+ puts "-- LIQUID C"
19
+ require 'liquid/c'
20
+ end
21
+
22
+ if Minitest.const_defined?('Test')
23
+ # We're on Minitest 5+. Nothing to do here.
24
+ else
25
+ # Minitest 4 doesn't have Minitest::Test yet.
26
+ Minitest::Test = MiniTest::Unit::TestCase
27
+ end
28
+
29
+ module Minitest
30
+ class Test
31
+ def fixture(name)
32
+ File.join(File.expand_path(__dir__), "fixtures", name)
33
+ end
34
+ end
35
+
36
+ module Assertions
37
+ include Liquid
38
+
39
+ def assert_template_result(expected, template, assigns = {}, message = nil)
40
+ assert_equal expected, Template.parse(template).render!(assigns), message
41
+ end
42
+
43
+ def assert_template_result_matches(expected, template, assigns = {}, message = nil)
44
+ return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp
45
+
46
+ assert_match expected, Template.parse(template).render!(assigns), message
47
+ end
48
+
49
+ def assert_match_syntax_error(match, template, assigns = {})
50
+ exception = assert_raises(Liquid::SyntaxError) do
51
+ Template.parse(template).render(assigns)
52
+ end
53
+ assert_match match, exception.message
54
+ end
55
+
56
+ def with_global_filter(*globals)
57
+ original_global_strainer = Liquid::Strainer.class_variable_get(:@@global_strainer)
58
+ Liquid::Strainer.class_variable_set(:@@global_strainer, Class.new(Liquid::Strainer) do
59
+ @filter_methods = Set.new
60
+ end)
61
+ Liquid::Strainer.class_variable_get(:@@strainer_class_cache).clear
62
+
63
+ globals.each do |global|
64
+ Liquid::Template.register_filter(global)
65
+ end
66
+ yield
67
+ ensure
68
+ Liquid::Strainer.class_variable_get(:@@strainer_class_cache).clear
69
+ Liquid::Strainer.class_variable_set(:@@global_strainer, original_global_strainer)
70
+ end
71
+
72
+ def with_taint_mode(mode)
73
+ old_mode = Liquid::Template.taint_mode
74
+ Liquid::Template.taint_mode = mode
75
+ yield
76
+ ensure
77
+ Liquid::Template.taint_mode = old_mode
78
+ end
79
+
80
+ def with_error_mode(mode)
81
+ old_mode = Liquid::Template.error_mode
82
+ Liquid::Template.error_mode = mode
83
+ yield
84
+ ensure
85
+ Liquid::Template.error_mode = old_mode
86
+ end
87
+ end
88
+ end
89
+
90
+ class ThingWithToLiquid
91
+ def to_liquid
92
+ 'foobar'
93
+ end
94
+ end
95
+
96
+ class ErrorDrop < Liquid::Drop
97
+ def standard_error
98
+ raise Liquid::StandardError, 'standard error'
99
+ end
100
+
101
+ def argument_error
102
+ raise Liquid::ArgumentError, 'argument error'
103
+ end
104
+
105
+ def syntax_error
106
+ raise Liquid::SyntaxError, 'syntax error'
107
+ end
108
+
109
+ def runtime_error
110
+ raise 'runtime error'
111
+ end
112
+
113
+ def exception
114
+ raise Exception, 'exception'
115
+ end
116
+ end
@@ -0,0 +1,58 @@
1
+ require 'test_helper'
2
+
3
+ class BlockUnitTest < Minitest::Test
4
+ include Liquid
5
+
6
+ def test_blankspace
7
+ template = Liquid::Template.parse(" ")
8
+ assert_equal [" "], template.root.nodelist
9
+ end
10
+
11
+ def test_variable_beginning
12
+ template = Liquid::Template.parse("{{funk}} ")
13
+ assert_equal 2, template.root.nodelist.size
14
+ assert_equal Variable, template.root.nodelist[0].class
15
+ assert_equal String, template.root.nodelist[1].class
16
+ end
17
+
18
+ def test_variable_end
19
+ template = Liquid::Template.parse(" {{funk}}")
20
+ assert_equal 2, template.root.nodelist.size
21
+ assert_equal String, template.root.nodelist[0].class
22
+ assert_equal Variable, template.root.nodelist[1].class
23
+ end
24
+
25
+ def test_variable_middle
26
+ template = Liquid::Template.parse(" {{funk}} ")
27
+ assert_equal 3, template.root.nodelist.size
28
+ assert_equal String, template.root.nodelist[0].class
29
+ assert_equal Variable, template.root.nodelist[1].class
30
+ assert_equal String, template.root.nodelist[2].class
31
+ end
32
+
33
+ def test_variable_many_embedded_fragments
34
+ template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ")
35
+ assert_equal 7, template.root.nodelist.size
36
+ assert_equal [String, Variable, String, Variable, String, Variable, String],
37
+ block_types(template.root.nodelist)
38
+ end
39
+
40
+ def test_with_block
41
+ template = Liquid::Template.parse(" {% comment %} {% endcomment %} ")
42
+ assert_equal [String, Comment, String], block_types(template.root.nodelist)
43
+ assert_equal 3, template.root.nodelist.size
44
+ end
45
+
46
+ def test_with_custom_tag
47
+ Liquid::Template.register_tag("testtag", Block)
48
+ assert Liquid::Template.parse("{% testtag %} {% endtesttag %}")
49
+ ensure
50
+ Liquid::Template.tags.delete('testtag')
51
+ end
52
+
53
+ private
54
+
55
+ def block_types(nodelist)
56
+ nodelist.collect(&:class)
57
+ end
58
+ end # VariableTest
@@ -0,0 +1,166 @@
1
+ require 'test_helper'
2
+
3
+ class ConditionUnitTest < Minitest::Test
4
+ include Liquid
5
+
6
+ def setup
7
+ @context = Liquid::Context.new
8
+ end
9
+
10
+ def test_basic_condition
11
+ assert_equal false, Condition.new(1, '==', 2).evaluate
12
+ assert_equal true, Condition.new(1, '==', 1).evaluate
13
+ end
14
+
15
+ def test_default_operators_evalute_true
16
+ assert_evaluates_true 1, '==', 1
17
+ assert_evaluates_true 1, '!=', 2
18
+ assert_evaluates_true 1, '<>', 2
19
+ assert_evaluates_true 1, '<', 2
20
+ assert_evaluates_true 2, '>', 1
21
+ assert_evaluates_true 1, '>=', 1
22
+ assert_evaluates_true 2, '>=', 1
23
+ assert_evaluates_true 1, '<=', 2
24
+ assert_evaluates_true 1, '<=', 1
25
+ # negative numbers
26
+ assert_evaluates_true 1, '>', -1
27
+ assert_evaluates_true (-1), '<', 1
28
+ assert_evaluates_true 1.0, '>', -1.0
29
+ assert_evaluates_true (-1.0), '<', 1.0
30
+ end
31
+
32
+ def test_default_operators_evalute_false
33
+ assert_evaluates_false 1, '==', 2
34
+ assert_evaluates_false 1, '!=', 1
35
+ assert_evaluates_false 1, '<>', 1
36
+ assert_evaluates_false 1, '<', 0
37
+ assert_evaluates_false 2, '>', 4
38
+ assert_evaluates_false 1, '>=', 3
39
+ assert_evaluates_false 2, '>=', 4
40
+ assert_evaluates_false 1, '<=', 0
41
+ assert_evaluates_false 1, '<=', 0
42
+ end
43
+
44
+ def test_contains_works_on_strings
45
+ assert_evaluates_true 'bob', 'contains', 'o'
46
+ assert_evaluates_true 'bob', 'contains', 'b'
47
+ assert_evaluates_true 'bob', 'contains', 'bo'
48
+ assert_evaluates_true 'bob', 'contains', 'ob'
49
+ assert_evaluates_true 'bob', 'contains', 'bob'
50
+
51
+ assert_evaluates_false 'bob', 'contains', 'bob2'
52
+ assert_evaluates_false 'bob', 'contains', 'a'
53
+ assert_evaluates_false 'bob', 'contains', '---'
54
+ end
55
+
56
+ def test_invalid_comparation_operator
57
+ assert_evaluates_argument_error 1, '~~', 0
58
+ end
59
+
60
+ def test_comparation_of_int_and_str
61
+ assert_evaluates_argument_error '1', '>', 0
62
+ assert_evaluates_argument_error '1', '<', 0
63
+ assert_evaluates_argument_error '1', '>=', 0
64
+ assert_evaluates_argument_error '1', '<=', 0
65
+ end
66
+
67
+ def test_hash_compare_backwards_compatibility
68
+ assert_nil Condition.new({}, '>', 2).evaluate
69
+ assert_nil Condition.new(2, '>', {}).evaluate
70
+ assert_equal false, Condition.new({}, '==', 2).evaluate
71
+ assert_equal true, Condition.new({ 'a' => 1 }, '==', { 'a' => 1 }).evaluate
72
+ assert_equal true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate
73
+ end
74
+
75
+ def test_contains_works_on_arrays
76
+ @context = Liquid::Context.new
77
+ @context['array'] = [1, 2, 3, 4, 5]
78
+ array_expr = VariableLookup.new("array")
79
+
80
+ assert_evaluates_false array_expr, 'contains', 0
81
+ assert_evaluates_true array_expr, 'contains', 1
82
+ assert_evaluates_true array_expr, 'contains', 2
83
+ assert_evaluates_true array_expr, 'contains', 3
84
+ assert_evaluates_true array_expr, 'contains', 4
85
+ assert_evaluates_true array_expr, 'contains', 5
86
+ assert_evaluates_false array_expr, 'contains', 6
87
+ assert_evaluates_false array_expr, 'contains', "1"
88
+ end
89
+
90
+ def test_contains_returns_false_for_nil_operands
91
+ @context = Liquid::Context.new
92
+ assert_evaluates_false VariableLookup.new('not_assigned'), 'contains', '0'
93
+ assert_evaluates_false 0, 'contains', VariableLookup.new('not_assigned')
94
+ end
95
+
96
+ def test_contains_return_false_on_wrong_data_type
97
+ assert_evaluates_false 1, 'contains', 0
98
+ end
99
+
100
+ def test_contains_with_string_left_operand_coerces_right_operand_to_string
101
+ assert_evaluates_true ' 1 ', 'contains', 1
102
+ assert_evaluates_false ' 1 ', 'contains', 2
103
+ end
104
+
105
+ def test_or_condition
106
+ condition = Condition.new(1, '==', 2)
107
+
108
+ assert_equal false, condition.evaluate
109
+
110
+ condition.or Condition.new(2, '==', 1)
111
+
112
+ assert_equal false, condition.evaluate
113
+
114
+ condition.or Condition.new(1, '==', 1)
115
+
116
+ assert_equal true, condition.evaluate
117
+ end
118
+
119
+ def test_and_condition
120
+ condition = Condition.new(1, '==', 1)
121
+
122
+ assert_equal true, condition.evaluate
123
+
124
+ condition.and Condition.new(2, '==', 2)
125
+
126
+ assert_equal true, condition.evaluate
127
+
128
+ condition.and Condition.new(2, '==', 1)
129
+
130
+ assert_equal false, condition.evaluate
131
+ end
132
+
133
+ def test_should_allow_custom_proc_operator
134
+ Condition.operators['starts_with'] = proc { |cond, left, right| left =~ %r{^#{right}} }
135
+
136
+ assert_evaluates_true 'bob', 'starts_with', 'b'
137
+ assert_evaluates_false 'bob', 'starts_with', 'o'
138
+ ensure
139
+ Condition.operators.delete 'starts_with'
140
+ end
141
+
142
+ def test_left_or_right_may_contain_operators
143
+ @context = Liquid::Context.new
144
+ @context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
145
+
146
+ assert_evaluates_true VariableLookup.new("one"), '==', VariableLookup.new("another")
147
+ end
148
+
149
+ private
150
+
151
+ def assert_evaluates_true(left, op, right)
152
+ assert Condition.new(left, op, right).evaluate(@context),
153
+ "Evaluated false: #{left} #{op} #{right}"
154
+ end
155
+
156
+ def assert_evaluates_false(left, op, right)
157
+ assert !Condition.new(left, op, right).evaluate(@context),
158
+ "Evaluated true: #{left} #{op} #{right}"
159
+ end
160
+
161
+ def assert_evaluates_argument_error(left, op, right)
162
+ assert_raises(Liquid::ArgumentError) do
163
+ Condition.new(left, op, right).evaluate(@context)
164
+ end
165
+ end
166
+ end # ConditionTest
@@ -0,0 +1,489 @@
1
+ require 'test_helper'
2
+
3
+ class HundredCentes
4
+ def to_liquid
5
+ 100
6
+ end
7
+ end
8
+
9
+ class CentsDrop < Liquid::Drop
10
+ def amount
11
+ HundredCentes.new
12
+ end
13
+
14
+ def non_zero?
15
+ true
16
+ end
17
+ end
18
+
19
+ class ContextSensitiveDrop < Liquid::Drop
20
+ def test
21
+ @context['test']
22
+ end
23
+ end
24
+
25
+ class Category < Liquid::Drop
26
+ attr_accessor :name
27
+
28
+ def initialize(name)
29
+ @name = name
30
+ end
31
+
32
+ def to_liquid
33
+ CategoryDrop.new(self)
34
+ end
35
+ end
36
+
37
+ class CategoryDrop
38
+ attr_accessor :category, :context
39
+ def initialize(category)
40
+ @category = category
41
+ end
42
+ end
43
+
44
+ class CounterDrop < Liquid::Drop
45
+ def count
46
+ @count ||= 0
47
+ @count += 1
48
+ end
49
+ end
50
+
51
+ class ArrayLike
52
+ def fetch(index)
53
+ end
54
+
55
+ def [](index)
56
+ @counts ||= []
57
+ @counts[index] ||= 0
58
+ @counts[index] += 1
59
+ end
60
+
61
+ def to_liquid
62
+ self
63
+ end
64
+ end
65
+
66
+ class ContextUnitTest < Minitest::Test
67
+ include Liquid
68
+
69
+ def setup
70
+ @context = Liquid::Context.new
71
+ end
72
+
73
+ def test_variables
74
+ @context['string'] = 'string'
75
+ assert_equal 'string', @context['string']
76
+
77
+ @context['num'] = 5
78
+ assert_equal 5, @context['num']
79
+
80
+ @context['time'] = Time.parse('2006-06-06 12:00:00')
81
+ assert_equal Time.parse('2006-06-06 12:00:00'), @context['time']
82
+
83
+ @context['date'] = Date.today
84
+ assert_equal Date.today, @context['date']
85
+
86
+ now = DateTime.now
87
+ @context['datetime'] = now
88
+ assert_equal now, @context['datetime']
89
+
90
+ @context['bool'] = true
91
+ assert_equal true, @context['bool']
92
+
93
+ @context['bool'] = false
94
+ assert_equal false, @context['bool']
95
+
96
+ @context['nil'] = nil
97
+ assert_nil @context['nil']
98
+ assert_nil @context['nil']
99
+ end
100
+
101
+ def test_variables_not_existing
102
+ assert_nil @context['does_not_exist']
103
+ end
104
+
105
+ def test_scoping
106
+ @context.push
107
+ @context.pop
108
+
109
+ assert_raises(Liquid::ContextError) do
110
+ @context.pop
111
+ end
112
+
113
+ assert_raises(Liquid::ContextError) do
114
+ @context.push
115
+ @context.pop
116
+ @context.pop
117
+ end
118
+ end
119
+
120
+ def test_length_query
121
+ @context['numbers'] = [1, 2, 3, 4]
122
+
123
+ assert_equal 4, @context['numbers.size']
124
+
125
+ @context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4 }
126
+
127
+ assert_equal 4, @context['numbers.size']
128
+
129
+ @context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 }
130
+
131
+ assert_equal 1000, @context['numbers.size']
132
+ end
133
+
134
+ def test_hyphenated_variable
135
+ @context['oh-my'] = 'godz'
136
+ assert_equal 'godz', @context['oh-my']
137
+ end
138
+
139
+ def test_add_filter
140
+ filter = Module.new do
141
+ def hi(output)
142
+ output + ' hi!'
143
+ end
144
+ end
145
+
146
+ context = Context.new
147
+ context.add_filters(filter)
148
+ assert_equal 'hi? hi!', context.invoke(:hi, 'hi?')
149
+
150
+ context = Context.new
151
+ assert_equal 'hi?', context.invoke(:hi, 'hi?')
152
+
153
+ context.add_filters(filter)
154
+ assert_equal 'hi? hi!', context.invoke(:hi, 'hi?')
155
+ end
156
+
157
+ def test_only_intended_filters_make_it_there
158
+ filter = Module.new do
159
+ def hi(output)
160
+ output + ' hi!'
161
+ end
162
+ end
163
+
164
+ context = Context.new
165
+ assert_equal "Wookie", context.invoke("hi", "Wookie")
166
+
167
+ context.add_filters(filter)
168
+ assert_equal "Wookie hi!", context.invoke("hi", "Wookie")
169
+ end
170
+
171
+ def test_add_item_in_outer_scope
172
+ @context['test'] = 'test'
173
+ @context.push
174
+ assert_equal 'test', @context['test']
175
+ @context.pop
176
+ assert_equal 'test', @context['test']
177
+ end
178
+
179
+ def test_add_item_in_inner_scope
180
+ @context.push
181
+ @context['test'] = 'test'
182
+ assert_equal 'test', @context['test']
183
+ @context.pop
184
+ assert_nil @context['test']
185
+ end
186
+
187
+ def test_hierachical_data
188
+ @context['hash'] = { "name" => 'tobi' }
189
+ assert_equal 'tobi', @context['hash.name']
190
+ assert_equal 'tobi', @context['hash["name"]']
191
+ end
192
+
193
+ def test_keywords
194
+ assert_equal true, @context['true']
195
+ assert_equal false, @context['false']
196
+ end
197
+
198
+ def test_digits
199
+ assert_equal 100, @context['100']
200
+ assert_equal 100.00, @context['100.00']
201
+ end
202
+
203
+ def test_strings
204
+ assert_equal "hello!", @context['"hello!"']
205
+ assert_equal "hello!", @context["'hello!'"]
206
+ end
207
+
208
+ def test_merge
209
+ @context.merge({ "test" => "test" })
210
+ assert_equal 'test', @context['test']
211
+ @context.merge({ "test" => "newvalue", "foo" => "bar" })
212
+ assert_equal 'newvalue', @context['test']
213
+ assert_equal 'bar', @context['foo']
214
+ end
215
+
216
+ def test_array_notation
217
+ @context['test'] = [1, 2, 3, 4, 5]
218
+
219
+ assert_equal 1, @context['test[0]']
220
+ assert_equal 2, @context['test[1]']
221
+ assert_equal 3, @context['test[2]']
222
+ assert_equal 4, @context['test[3]']
223
+ assert_equal 5, @context['test[4]']
224
+ end
225
+
226
+ def test_recoursive_array_notation
227
+ @context['test'] = { 'test' => [1, 2, 3, 4, 5] }
228
+
229
+ assert_equal 1, @context['test.test[0]']
230
+
231
+ @context['test'] = [{ 'test' => 'worked' }]
232
+
233
+ assert_equal 'worked', @context['test[0].test']
234
+ end
235
+
236
+ def test_hash_to_array_transition
237
+ @context['colors'] = {
238
+ 'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
239
+ 'Green' => ['003300', '336633', '669966', '99CC99'],
240
+ 'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
241
+ 'Red' => ['660000', '993333', 'CC6666', 'FF9999']
242
+ }
243
+
244
+ assert_equal '003366', @context['colors.Blue[0]']
245
+ assert_equal 'FF9999', @context['colors.Red[3]']
246
+ end
247
+
248
+ def test_try_first
249
+ @context['test'] = [1, 2, 3, 4, 5]
250
+
251
+ assert_equal 1, @context['test.first']
252
+ assert_equal 5, @context['test.last']
253
+
254
+ @context['test'] = { 'test' => [1, 2, 3, 4, 5] }
255
+
256
+ assert_equal 1, @context['test.test.first']
257
+ assert_equal 5, @context['test.test.last']
258
+
259
+ @context['test'] = [1]
260
+ assert_equal 1, @context['test.first']
261
+ assert_equal 1, @context['test.last']
262
+ end
263
+
264
+ def test_access_hashes_with_hash_notation
265
+ @context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
266
+ @context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] }
267
+
268
+ assert_equal 5, @context['products["count"]']
269
+ assert_equal 'deepsnow', @context['products["tags"][0]']
270
+ assert_equal 'deepsnow', @context['products["tags"].first']
271
+ assert_equal 'draft151cm', @context['product["variants"][0]["title"]']
272
+ assert_equal 'element151cm', @context['product["variants"][1]["title"]']
273
+ assert_equal 'draft151cm', @context['product["variants"][0]["title"]']
274
+ assert_equal 'element151cm', @context['product["variants"].last["title"]']
275
+ end
276
+
277
+ def test_access_variable_with_hash_notation
278
+ @context['foo'] = 'baz'
279
+ @context['bar'] = 'foo'
280
+
281
+ assert_equal 'baz', @context['["foo"]']
282
+ assert_equal 'baz', @context['[bar]']
283
+ end
284
+
285
+ def test_access_hashes_with_hash_access_variables
286
+ @context['var'] = 'tags'
287
+ @context['nested'] = { 'var' => 'tags' }
288
+ @context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
289
+
290
+ assert_equal 'deepsnow', @context['products[var].first']
291
+ assert_equal 'freestyle', @context['products[nested.var].last']
292
+ end
293
+
294
+ def test_hash_notation_only_for_hash_access
295
+ @context['array'] = [1, 2, 3, 4, 5]
296
+ @context['hash'] = { 'first' => 'Hello' }
297
+
298
+ assert_equal 1, @context['array.first']
299
+ assert_nil @context['array["first"]']
300
+ assert_equal 'Hello', @context['hash["first"]']
301
+ end
302
+
303
+ def test_first_can_appear_in_middle_of_callchain
304
+ @context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] }
305
+
306
+ assert_equal 'draft151cm', @context['product.variants[0].title']
307
+ assert_equal 'element151cm', @context['product.variants[1].title']
308
+ assert_equal 'draft151cm', @context['product.variants.first.title']
309
+ assert_equal 'element151cm', @context['product.variants.last.title']
310
+ end
311
+
312
+ def test_cents
313
+ @context.merge("cents" => HundredCentes.new)
314
+ assert_equal 100, @context['cents']
315
+ end
316
+
317
+ def test_nested_cents
318
+ @context.merge("cents" => { 'amount' => HundredCentes.new })
319
+ assert_equal 100, @context['cents.amount']
320
+
321
+ @context.merge("cents" => { 'cents' => { 'amount' => HundredCentes.new } })
322
+ assert_equal 100, @context['cents.cents.amount']
323
+ end
324
+
325
+ def test_cents_through_drop
326
+ @context.merge("cents" => CentsDrop.new)
327
+ assert_equal 100, @context['cents.amount']
328
+ end
329
+
330
+ def test_nested_cents_through_drop
331
+ @context.merge("vars" => { "cents" => CentsDrop.new })
332
+ assert_equal 100, @context['vars.cents.amount']
333
+ end
334
+
335
+ def test_drop_methods_with_question_marks
336
+ @context.merge("cents" => CentsDrop.new)
337
+ assert @context['cents.non_zero?']
338
+ end
339
+
340
+ def test_context_from_within_drop
341
+ @context.merge("test" => '123', "vars" => ContextSensitiveDrop.new)
342
+ assert_equal '123', @context['vars.test']
343
+ end
344
+
345
+ def test_nested_context_from_within_drop
346
+ @context.merge("test" => '123', "vars" => { "local" => ContextSensitiveDrop.new })
347
+ assert_equal '123', @context['vars.local.test']
348
+ end
349
+
350
+ def test_ranges
351
+ @context.merge("test" => '5')
352
+ assert_equal (1..5), @context['(1..5)']
353
+ assert_equal (1..5), @context['(1..test)']
354
+ assert_equal (5..5), @context['(test..test)']
355
+ end
356
+
357
+ def test_cents_through_drop_nestedly
358
+ @context.merge("cents" => { "cents" => CentsDrop.new })
359
+ assert_equal 100, @context['cents.cents.amount']
360
+
361
+ @context.merge("cents" => { "cents" => { "cents" => CentsDrop.new } })
362
+ assert_equal 100, @context['cents.cents.cents.amount']
363
+ end
364
+
365
+ def test_drop_with_variable_called_only_once
366
+ @context['counter'] = CounterDrop.new
367
+
368
+ assert_equal 1, @context['counter.count']
369
+ assert_equal 2, @context['counter.count']
370
+ assert_equal 3, @context['counter.count']
371
+ end
372
+
373
+ def test_drop_with_key_called_only_once
374
+ @context['counter'] = CounterDrop.new
375
+
376
+ assert_equal 1, @context['counter["count"]']
377
+ assert_equal 2, @context['counter["count"]']
378
+ assert_equal 3, @context['counter["count"]']
379
+ end
380
+
381
+ def test_proc_as_variable
382
+ @context['dynamic'] = proc { 'Hello' }
383
+
384
+ assert_equal 'Hello', @context['dynamic']
385
+ end
386
+
387
+ def test_lambda_as_variable
388
+ @context['dynamic'] = proc { 'Hello' }
389
+
390
+ assert_equal 'Hello', @context['dynamic']
391
+ end
392
+
393
+ def test_nested_lambda_as_variable
394
+ @context['dynamic'] = { "lambda" => proc { 'Hello' } }
395
+
396
+ assert_equal 'Hello', @context['dynamic.lambda']
397
+ end
398
+
399
+ def test_array_containing_lambda_as_variable
400
+ @context['dynamic'] = [1, 2, proc { 'Hello' }, 4, 5]
401
+
402
+ assert_equal 'Hello', @context['dynamic[2]']
403
+ end
404
+
405
+ def test_lambda_is_called_once
406
+ @context['callcount'] = proc { @global ||= 0; @global += 1; @global.to_s }
407
+
408
+ assert_equal '1', @context['callcount']
409
+ assert_equal '1', @context['callcount']
410
+ assert_equal '1', @context['callcount']
411
+
412
+ @global = nil
413
+ end
414
+
415
+ def test_nested_lambda_is_called_once
416
+ @context['callcount'] = { "lambda" => proc { @global ||= 0; @global += 1; @global.to_s } }
417
+
418
+ assert_equal '1', @context['callcount.lambda']
419
+ assert_equal '1', @context['callcount.lambda']
420
+ assert_equal '1', @context['callcount.lambda']
421
+
422
+ @global = nil
423
+ end
424
+
425
+ def test_lambda_in_array_is_called_once
426
+ @context['callcount'] = [1, 2, proc { @global ||= 0; @global += 1; @global.to_s }, 4, 5]
427
+
428
+ assert_equal '1', @context['callcount[2]']
429
+ assert_equal '1', @context['callcount[2]']
430
+ assert_equal '1', @context['callcount[2]']
431
+
432
+ @global = nil
433
+ end
434
+
435
+ def test_access_to_context_from_proc
436
+ @context.registers[:magic] = 345392
437
+
438
+ @context['magic'] = proc { @context.registers[:magic] }
439
+
440
+ assert_equal 345392, @context['magic']
441
+ end
442
+
443
+ def test_to_liquid_and_context_at_first_level
444
+ @context['category'] = Category.new("foobar")
445
+ assert_kind_of CategoryDrop, @context['category']
446
+ assert_equal @context, @context['category'].context
447
+ end
448
+
449
+ def test_interrupt_avoids_object_allocations
450
+ assert_no_object_allocations do
451
+ @context.interrupt?
452
+ end
453
+ end
454
+
455
+ def test_context_initialization_with_a_proc_in_environment
456
+ contx = Context.new([test: ->(c) { c['poutine'] }], { test: :foo })
457
+
458
+ assert contx
459
+ assert_nil contx['poutine']
460
+ end
461
+
462
+ def test_apply_global_filter
463
+ global_filter_proc = ->(output) { "#{output} filtered" }
464
+
465
+ context = Context.new
466
+ context.global_filter = global_filter_proc
467
+
468
+ assert_equal 'hi filtered', context.apply_global_filter('hi')
469
+ end
470
+
471
+ def test_apply_global_filter_when_no_global_filter_exist
472
+ context = Context.new
473
+ assert_equal 'hi', context.apply_global_filter('hi')
474
+ end
475
+
476
+ private
477
+
478
+ def assert_no_object_allocations
479
+ unless RUBY_ENGINE == 'ruby'
480
+ skip "stackprof needed to count object allocations"
481
+ end
482
+ require 'stackprof'
483
+
484
+ profile = StackProf.run(mode: :object) do
485
+ yield
486
+ end
487
+ assert_equal 0, profile[:samples]
488
+ end
489
+ end # ContextTest