liquid 4.0.3 → 5.0.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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +33 -0
  3. data/README.md +6 -0
  4. data/lib/liquid.rb +17 -5
  5. data/lib/liquid/block.rb +31 -14
  6. data/lib/liquid/block_body.rb +164 -54
  7. data/lib/liquid/condition.rb +39 -18
  8. data/lib/liquid/context.rb +106 -51
  9. data/lib/liquid/document.rb +47 -9
  10. data/lib/liquid/drop.rb +4 -2
  11. data/lib/liquid/errors.rb +20 -18
  12. data/lib/liquid/expression.rb +29 -34
  13. data/lib/liquid/extensions.rb +2 -0
  14. data/lib/liquid/file_system.rb +6 -4
  15. data/lib/liquid/forloop_drop.rb +11 -4
  16. data/lib/liquid/i18n.rb +5 -3
  17. data/lib/liquid/interrupts.rb +3 -1
  18. data/lib/liquid/lexer.rb +30 -23
  19. data/lib/liquid/locales/en.yml +3 -1
  20. data/lib/liquid/parse_context.rb +16 -4
  21. data/lib/liquid/parse_tree_visitor.rb +2 -2
  22. data/lib/liquid/parser.rb +30 -18
  23. data/lib/liquid/parser_switching.rb +17 -3
  24. data/lib/liquid/partial_cache.rb +24 -0
  25. data/lib/liquid/profiler.rb +67 -86
  26. data/lib/liquid/profiler/hooks.rb +26 -14
  27. data/lib/liquid/range_lookup.rb +5 -3
  28. data/lib/liquid/register.rb +6 -0
  29. data/lib/liquid/resource_limits.rb +47 -8
  30. data/lib/liquid/standardfilters.rb +63 -44
  31. data/lib/liquid/static_registers.rb +44 -0
  32. data/lib/liquid/strainer_factory.rb +36 -0
  33. data/lib/liquid/strainer_template.rb +53 -0
  34. data/lib/liquid/tablerowloop_drop.rb +6 -4
  35. data/lib/liquid/tag.rb +28 -6
  36. data/lib/liquid/tag/disableable.rb +22 -0
  37. data/lib/liquid/tag/disabler.rb +21 -0
  38. data/lib/liquid/tags/assign.rb +24 -10
  39. data/lib/liquid/tags/break.rb +8 -3
  40. data/lib/liquid/tags/capture.rb +11 -8
  41. data/lib/liquid/tags/case.rb +33 -27
  42. data/lib/liquid/tags/comment.rb +5 -3
  43. data/lib/liquid/tags/continue.rb +8 -3
  44. data/lib/liquid/tags/cycle.rb +25 -14
  45. data/lib/liquid/tags/decrement.rb +6 -3
  46. data/lib/liquid/tags/echo.rb +26 -0
  47. data/lib/liquid/tags/for.rb +68 -44
  48. data/lib/liquid/tags/if.rb +35 -23
  49. data/lib/liquid/tags/ifchanged.rb +11 -10
  50. data/lib/liquid/tags/include.rb +34 -47
  51. data/lib/liquid/tags/increment.rb +7 -3
  52. data/lib/liquid/tags/raw.rb +14 -11
  53. data/lib/liquid/tags/render.rb +84 -0
  54. data/lib/liquid/tags/table_row.rb +23 -19
  55. data/lib/liquid/tags/unless.rb +15 -15
  56. data/lib/liquid/template.rb +55 -71
  57. data/lib/liquid/template_factory.rb +9 -0
  58. data/lib/liquid/tokenizer.rb +17 -9
  59. data/lib/liquid/usage.rb +8 -0
  60. data/lib/liquid/utils.rb +5 -3
  61. data/lib/liquid/variable.rb +46 -41
  62. data/lib/liquid/variable_lookup.rb +8 -6
  63. data/lib/liquid/version.rb +2 -1
  64. data/test/integration/assign_test.rb +74 -5
  65. data/test/integration/blank_test.rb +11 -8
  66. data/test/integration/block_test.rb +47 -1
  67. data/test/integration/capture_test.rb +18 -10
  68. data/test/integration/context_test.rb +608 -5
  69. data/test/integration/document_test.rb +4 -2
  70. data/test/integration/drop_test.rb +67 -83
  71. data/test/integration/error_handling_test.rb +73 -61
  72. data/test/integration/expression_test.rb +46 -0
  73. data/test/integration/filter_test.rb +53 -42
  74. data/test/integration/hash_ordering_test.rb +5 -3
  75. data/test/integration/output_test.rb +26 -24
  76. data/test/integration/parsing_quirks_test.rb +19 -7
  77. data/test/integration/{render_profiling_test.rb → profiler_test.rb} +84 -25
  78. data/test/integration/security_test.rb +30 -21
  79. data/test/integration/standard_filter_test.rb +339 -281
  80. data/test/integration/tag/disableable_test.rb +59 -0
  81. data/test/integration/tag_test.rb +45 -0
  82. data/test/integration/tags/break_tag_test.rb +4 -2
  83. data/test/integration/tags/continue_tag_test.rb +4 -2
  84. data/test/integration/tags/echo_test.rb +13 -0
  85. data/test/integration/tags/for_tag_test.rb +107 -51
  86. data/test/integration/tags/if_else_tag_test.rb +5 -3
  87. data/test/integration/tags/include_tag_test.rb +70 -54
  88. data/test/integration/tags/increment_tag_test.rb +4 -2
  89. data/test/integration/tags/liquid_tag_test.rb +116 -0
  90. data/test/integration/tags/raw_tag_test.rb +14 -11
  91. data/test/integration/tags/render_tag_test.rb +213 -0
  92. data/test/integration/tags/standard_tag_test.rb +38 -31
  93. data/test/integration/tags/statements_test.rb +23 -21
  94. data/test/integration/tags/table_row_test.rb +2 -0
  95. data/test/integration/tags/unless_else_tag_test.rb +4 -2
  96. data/test/integration/template_test.rb +118 -124
  97. data/test/integration/trim_mode_test.rb +78 -44
  98. data/test/integration/variable_test.rb +43 -32
  99. data/test/test_helper.rb +75 -22
  100. data/test/unit/block_unit_test.rb +19 -24
  101. data/test/unit/condition_unit_test.rb +79 -77
  102. data/test/unit/file_system_unit_test.rb +6 -4
  103. data/test/unit/i18n_unit_test.rb +7 -5
  104. data/test/unit/lexer_unit_test.rb +11 -9
  105. data/test/{integration → unit}/parse_tree_visitor_test.rb +2 -2
  106. data/test/unit/parser_unit_test.rb +37 -35
  107. data/test/unit/partial_cache_unit_test.rb +128 -0
  108. data/test/unit/regexp_unit_test.rb +17 -15
  109. data/test/unit/static_registers_unit_test.rb +156 -0
  110. data/test/unit/strainer_factory_unit_test.rb +100 -0
  111. data/test/unit/strainer_template_unit_test.rb +82 -0
  112. data/test/unit/tag_unit_test.rb +5 -3
  113. data/test/unit/tags/case_tag_unit_test.rb +3 -1
  114. data/test/unit/tags/for_tag_unit_test.rb +4 -2
  115. data/test/unit/tags/if_tag_unit_test.rb +3 -1
  116. data/test/unit/template_factory_unit_test.rb +12 -0
  117. data/test/unit/template_unit_test.rb +19 -10
  118. data/test/unit/tokenizer_unit_test.rb +19 -17
  119. data/test/unit/variable_unit_test.rb +51 -49
  120. metadata +73 -47
  121. data/lib/liquid/strainer.rb +0 -66
  122. data/lib/liquid/truffle.rb +0 -5
  123. data/test/truffle/truffle_test.rb +0 -9
  124. data/test/unit/context_unit_test.rb +0 -489
  125. data/test/unit/strainer_unit_test.rb +0 -164
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class VariableLookup
3
5
  SQUARE_BRACKETED = /\A\[(.*)\]\z/m
4
- COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze].freeze
6
+ COMMAND_METHODS = ['size', 'first', 'last'].freeze
5
7
 
6
8
  attr_reader :name, :lookups
7
9
 
@@ -14,17 +16,17 @@ module Liquid
14
16
 
15
17
  name = lookups.shift
16
18
  if name =~ SQUARE_BRACKETED
17
- name = Expression.parse($1)
19
+ name = Expression.parse(Regexp.last_match(1))
18
20
  end
19
21
  @name = name
20
22
 
21
- @lookups = lookups
23
+ @lookups = lookups
22
24
  @command_flags = 0
23
25
 
24
26
  @lookups.each_index do |i|
25
27
  lookup = lookups[i]
26
28
  if lookup =~ SQUARE_BRACKETED
27
- lookups[i] = Expression.parse($1)
29
+ lookups[i] = Expression.parse(Regexp.last_match(1))
28
30
  elsif COMMAND_METHODS.include?(lookup)
29
31
  @command_flags |= 1 << i
30
32
  end
@@ -32,7 +34,7 @@ module Liquid
32
34
  end
33
35
 
34
36
  def evaluate(context)
35
- name = context.evaluate(@name)
37
+ name = context.evaluate(@name)
36
38
  object = context.find_variable(name)
37
39
 
38
40
  @lookups.each_index do |i|
@@ -45,7 +47,7 @@ module Liquid
45
47
  (object.respond_to?(:fetch) && key.is_a?(Integer)))
46
48
 
47
49
  # if its a proc we will replace the entry with the proc
48
- res = context.lookup_and_evaluate(object, key)
50
+ res = context.lookup_and_evaluate(object, key)
49
51
  object = res.to_liquid
50
52
 
51
53
  # Some special cases. If the part wasn't in square brackets and
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  module Liquid
4
- VERSION = "4.0.3".freeze
5
+ VERSION = "5.0.0"
5
6
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  class AssignTest < Minitest::Test
@@ -8,9 +10,9 @@ class AssignTest < Minitest::Test
8
10
  {% assign this-thing = 'Print this-thing' %}
9
11
  {{ this-thing }}
10
12
  END_TEMPLATE
11
- template = Template.parse(template_source)
12
- rendered = template.render!
13
- assert_equal "Print this-thing", rendered.strip
13
+ template = Template.parse(template_source)
14
+ rendered = template.render!
15
+ assert_equal("Print this-thing", rendered.strip)
14
16
  end
15
17
 
16
18
  def test_assigned_variable
@@ -42,7 +44,74 @@ class AssignTest < Minitest::Test
42
44
  end
43
45
  end
44
46
  with_error_mode(:lax) do
45
- assert Template.parse("{% assign foo = ('X' | downcase) %}")
47
+ assert(Template.parse("{% assign foo = ('X' | downcase) %}"))
48
+ end
49
+ end
50
+
51
+ def test_expression_with_whitespace_in_square_brackets
52
+ source = "{% assign r = a[ 'b' ] %}{{ r }}"
53
+ assert_template_result('result', source, 'a' => { 'b' => 'result' })
54
+ end
55
+
56
+ def test_assign_score_exceeding_resource_limit
57
+ t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
58
+ t.resource_limits.assign_score_limit = 1
59
+ assert_equal("Liquid error: Memory limits exceeded", t.render)
60
+ assert(t.resource_limits.reached?)
61
+
62
+ t.resource_limits.assign_score_limit = 2
63
+ assert_equal("", t.render!)
64
+ refute_nil(t.resource_limits.assign_score)
65
+ end
66
+
67
+ def test_assign_score_exceeding_limit_from_composite_object
68
+ t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
69
+
70
+ t.resource_limits.assign_score_limit = 3
71
+ assert_equal("Liquid error: Memory limits exceeded", t.render)
72
+ assert(t.resource_limits.reached?)
73
+
74
+ t.resource_limits.assign_score_limit = 5
75
+ assert_equal("", t.render!)
76
+ end
77
+
78
+ def test_assign_score_of_int
79
+ assert_equal(1, assign_score_of(123))
80
+ end
81
+
82
+ def test_assign_score_of_string_counts_bytes
83
+ assert_equal(3, assign_score_of('123'))
84
+ assert_equal(5, assign_score_of('12345'))
85
+ assert_equal(9, assign_score_of('すごい'))
86
+ end
87
+
88
+ def test_assign_score_of_array
89
+ assert_equal(1, assign_score_of([]))
90
+ assert_equal(2, assign_score_of([123]))
91
+ assert_equal(6, assign_score_of([123, 'abcd']))
92
+ end
93
+
94
+ def test_assign_score_of_hash
95
+ assert_equal(1, assign_score_of({}))
96
+ assert_equal(5, assign_score_of('int' => 123))
97
+ assert_equal(12, assign_score_of('int' => 123, 'str' => 'abcd'))
98
+ end
99
+
100
+ private
101
+
102
+ class ObjectWrapperDrop < Liquid::Drop
103
+ def initialize(obj)
104
+ @obj = obj
105
+ end
106
+
107
+ def value
108
+ @obj
46
109
  end
47
110
  end
48
- end # AssignTest
111
+
112
+ def assign_score_of(obj)
113
+ context = Liquid::Context.new('drop' => ObjectWrapperDrop.new(obj))
114
+ Liquid::Template.parse('{% assign obj = drop.value %}').render!(context)
115
+ context.resource_limits.assign_score
116
+ end
117
+ end
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  class FoobarTag < Liquid::Tag
4
- def render(*args)
5
- " "
6
+ def render_to_output_buffer(_context, output)
7
+ output << ' '
8
+ output
6
9
  end
7
-
8
- Liquid::Template.register_tag('foobar', FoobarTag)
9
10
  end
10
11
 
11
12
  class BlankTestFileSystem
@@ -31,7 +32,9 @@ class BlankTest < Minitest::Test
31
32
  end
32
33
 
33
34
  def test_new_tags_are_not_blank_by_default
34
- assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
35
+ with_custom_tag('foobar', FoobarTag) do
36
+ assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
37
+ end
35
38
  end
36
39
 
37
40
  def test_loops_are_blank
@@ -93,9 +96,9 @@ class BlankTest < Minitest::Test
93
96
 
94
97
  def test_include_is_blank
95
98
  Liquid::Template.file_system = BlankTestFileSystem.new
96
- assert_template_result "foobar" * (N + 1), wrap("{% include 'foobar' %}")
97
- assert_template_result " foobar " * (N + 1), wrap("{% include ' foobar ' %}")
98
- assert_template_result " " * (N + 1), wrap(" {% include ' ' %} ")
99
+ assert_template_result("foobar" * (N + 1), wrap("{% include 'foobar' %}"))
100
+ assert_template_result(" foobar " * (N + 1), wrap("{% include ' foobar ' %}"))
101
+ assert_template_result(" " * (N + 1), wrap(" {% include ' ' %} "))
99
102
  end
100
103
 
101
104
  def test_case_is_blank
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  class BlockTest < Minitest::Test
@@ -7,6 +9,50 @@ class BlockTest < Minitest::Test
7
9
  exc = assert_raises(SyntaxError) do
8
10
  Template.parse("{% if true %}{% endunless %}")
9
11
  end
10
- assert_equal exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif"
12
+ assert_equal(exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif")
13
+ end
14
+
15
+ def test_with_custom_tag
16
+ with_custom_tag('testtag', Block) do
17
+ assert(Liquid::Template.parse("{% testtag %} {% endtesttag %}"))
18
+ end
19
+ end
20
+
21
+ def test_custom_block_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
22
+ klass1 = Class.new(Block) do
23
+ def render(*)
24
+ 'hello'
25
+ end
26
+ end
27
+
28
+ with_custom_tag('blabla', klass1) do
29
+ template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}")
30
+
31
+ assert_equal('hello', template.render)
32
+
33
+ buf = +''
34
+ output = template.render({}, output: buf)
35
+ assert_equal('hello', output)
36
+ assert_equal('hello', buf)
37
+ assert_equal(buf.object_id, output.object_id)
38
+ end
39
+
40
+ klass2 = Class.new(klass1) do
41
+ def render(*)
42
+ 'foo' + super + 'bar'
43
+ end
44
+ end
45
+
46
+ with_custom_tag('blabla', klass2) do
47
+ template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}")
48
+
49
+ assert_equal('foohellobar', template.render)
50
+
51
+ buf = +''
52
+ output = template.render({}, output: buf)
53
+ assert_equal('foohellobar', output)
54
+ assert_equal('foohellobar', buf)
55
+ assert_equal(buf.object_id, output.object_id)
56
+ end
11
57
  end
12
58
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  class CaptureTest < Minitest::Test
@@ -12,9 +14,9 @@ class CaptureTest < Minitest::Test
12
14
  {% capture this-thing %}Print this-thing{% endcapture %}
13
15
  {{ this-thing }}
14
16
  END_TEMPLATE
15
- template = Template.parse(template_source)
16
- rendered = template.render!
17
- assert_equal "Print this-thing", rendered.strip
17
+ template = Template.parse(template_source)
18
+ rendered = template.render!
19
+ assert_equal("Print this-thing", rendered.strip)
18
20
  end
19
21
 
20
22
  def test_capture_to_variable_from_outer_scope_if_existing
@@ -28,9 +30,9 @@ class CaptureTest < Minitest::Test
28
30
  {% endif %}
29
31
  {{var}}
30
32
  END_TEMPLATE
31
- template = Template.parse(template_source)
32
- rendered = template.render!
33
- assert_equal "test-string", rendered.gsub(/\s/, '')
33
+ template = Template.parse(template_source)
34
+ rendered = template.render!
35
+ assert_equal("test-string", rendered.gsub(/\s/, ''))
34
36
  end
35
37
 
36
38
  def test_assigning_from_capture
@@ -43,8 +45,14 @@ class CaptureTest < Minitest::Test
43
45
  {% endfor %}
44
46
  {{ first }}-{{ second }}
45
47
  END_TEMPLATE
46
- template = Template.parse(template_source)
47
- rendered = template.render!
48
- assert_equal "3-3", rendered.gsub(/\s/, '')
48
+ template = Template.parse(template_source)
49
+ rendered = template.render!
50
+ assert_equal("3-3", rendered.gsub(/\s/, ''))
51
+ end
52
+
53
+ def test_increment_assign_score_by_bytes_not_characters
54
+ t = Template.parse("{% capture foo %}すごい{% endcapture %}")
55
+ t.render!
56
+ assert_equal(9, t.resource_limits.assign_score)
49
57
  end
50
- end # CaptureTest
58
+ end
@@ -1,8 +1,597 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
5
+ class HundredCentes
6
+ def to_liquid
7
+ 100
8
+ end
9
+ end
10
+
11
+ class CentsDrop < Liquid::Drop
12
+ def amount
13
+ HundredCentes.new
14
+ end
15
+
16
+ def non_zero?
17
+ true
18
+ end
19
+ end
20
+
21
+ class ContextSensitiveDrop < Liquid::Drop
22
+ def test
23
+ @context['test']
24
+ end
25
+ end
26
+
27
+ class Category < Liquid::Drop
28
+ attr_accessor :name
29
+
30
+ def initialize(name)
31
+ @name = name
32
+ end
33
+
34
+ def to_liquid
35
+ CategoryDrop.new(self)
36
+ end
37
+ end
38
+
39
+ class CategoryDrop
40
+ attr_accessor :category, :context
41
+ def initialize(category)
42
+ @category = category
43
+ end
44
+ end
45
+
46
+ class CounterDrop < Liquid::Drop
47
+ def count
48
+ @count ||= 0
49
+ @count += 1
50
+ end
51
+ end
52
+
53
+ class ArrayLike
54
+ def fetch(index)
55
+ end
56
+
57
+ def [](index)
58
+ @counts ||= []
59
+ @counts[index] ||= 0
60
+ @counts[index] += 1
61
+ end
62
+
63
+ def to_liquid
64
+ self
65
+ end
66
+ end
67
+
3
68
  class ContextTest < Minitest::Test
4
69
  include Liquid
5
70
 
71
+ def setup
72
+ @context = Liquid::Context.new
73
+ end
74
+
75
+ def test_variables
76
+ @context['string'] = 'string'
77
+ assert_equal('string', @context['string'])
78
+
79
+ @context['num'] = 5
80
+ assert_equal(5, @context['num'])
81
+
82
+ @context['time'] = Time.parse('2006-06-06 12:00:00')
83
+ assert_equal(Time.parse('2006-06-06 12:00:00'), @context['time'])
84
+
85
+ @context['date'] = Date.today
86
+ assert_equal(Date.today, @context['date'])
87
+
88
+ now = Time.now
89
+ @context['datetime'] = now
90
+ assert_equal(now, @context['datetime'])
91
+
92
+ @context['bool'] = true
93
+ assert_equal(true, @context['bool'])
94
+
95
+ @context['bool'] = false
96
+ assert_equal(false, @context['bool'])
97
+
98
+ @context['nil'] = nil
99
+ assert_nil(@context['nil'])
100
+ assert_nil(@context['nil'])
101
+ end
102
+
103
+ def test_variables_not_existing
104
+ assert_nil(@context['does_not_exist'])
105
+ end
106
+
107
+ def test_scoping
108
+ @context.push
109
+ @context.pop
110
+
111
+ assert_raises(Liquid::ContextError) do
112
+ @context.pop
113
+ end
114
+
115
+ assert_raises(Liquid::ContextError) do
116
+ @context.push
117
+ @context.pop
118
+ @context.pop
119
+ end
120
+ end
121
+
122
+ def test_length_query
123
+ @context['numbers'] = [1, 2, 3, 4]
124
+
125
+ assert_equal(4, @context['numbers.size'])
126
+
127
+ @context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4 }
128
+
129
+ assert_equal(4, @context['numbers.size'])
130
+
131
+ @context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 }
132
+
133
+ assert_equal(1000, @context['numbers.size'])
134
+ end
135
+
136
+ def test_hyphenated_variable
137
+ @context['oh-my'] = 'godz'
138
+ assert_equal('godz', @context['oh-my'])
139
+ end
140
+
141
+ def test_add_filter
142
+ filter = Module.new do
143
+ def hi(output)
144
+ output + ' hi!'
145
+ end
146
+ end
147
+
148
+ context = Context.new
149
+ context.add_filters(filter)
150
+ assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
151
+
152
+ context = Context.new
153
+ assert_equal('hi?', context.invoke(:hi, 'hi?'))
154
+
155
+ context.add_filters(filter)
156
+ assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
157
+ end
158
+
159
+ def test_only_intended_filters_make_it_there
160
+ filter = Module.new do
161
+ def hi(output)
162
+ output + ' hi!'
163
+ end
164
+ end
165
+
166
+ context = Context.new
167
+ assert_equal("Wookie", context.invoke("hi", "Wookie"))
168
+
169
+ context.add_filters(filter)
170
+ assert_equal("Wookie hi!", context.invoke("hi", "Wookie"))
171
+ end
172
+
173
+ def test_add_item_in_outer_scope
174
+ @context['test'] = 'test'
175
+ @context.push
176
+ assert_equal('test', @context['test'])
177
+ @context.pop
178
+ assert_equal('test', @context['test'])
179
+ end
180
+
181
+ def test_add_item_in_inner_scope
182
+ @context.push
183
+ @context['test'] = 'test'
184
+ assert_equal('test', @context['test'])
185
+ @context.pop
186
+ assert_nil(@context['test'])
187
+ end
188
+
189
+ def test_hierachical_data
190
+ @context['hash'] = { "name" => 'tobi' }
191
+ assert_equal('tobi', @context['hash.name'])
192
+ assert_equal('tobi', @context['hash["name"]'])
193
+ end
194
+
195
+ def test_keywords
196
+ assert_equal(true, @context['true'])
197
+ assert_equal(false, @context['false'])
198
+ end
199
+
200
+ def test_digits
201
+ assert_equal(100, @context['100'])
202
+ assert_equal(100.00, @context['100.00'])
203
+ end
204
+
205
+ def test_strings
206
+ assert_equal("hello!", @context['"hello!"'])
207
+ assert_equal("hello!", @context["'hello!'"])
208
+ end
209
+
210
+ def test_merge
211
+ @context.merge("test" => "test")
212
+ assert_equal('test', @context['test'])
213
+ @context.merge("test" => "newvalue", "foo" => "bar")
214
+ assert_equal('newvalue', @context['test'])
215
+ assert_equal('bar', @context['foo'])
216
+ end
217
+
218
+ def test_array_notation
219
+ @context['test'] = [1, 2, 3, 4, 5]
220
+
221
+ assert_equal(1, @context['test[0]'])
222
+ assert_equal(2, @context['test[1]'])
223
+ assert_equal(3, @context['test[2]'])
224
+ assert_equal(4, @context['test[3]'])
225
+ assert_equal(5, @context['test[4]'])
226
+ end
227
+
228
+ def test_recoursive_array_notation
229
+ @context['test'] = { 'test' => [1, 2, 3, 4, 5] }
230
+
231
+ assert_equal(1, @context['test.test[0]'])
232
+
233
+ @context['test'] = [{ 'test' => 'worked' }]
234
+
235
+ assert_equal('worked', @context['test[0].test'])
236
+ end
237
+
238
+ def test_hash_to_array_transition
239
+ @context['colors'] = {
240
+ 'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
241
+ 'Green' => ['003300', '336633', '669966', '99CC99'],
242
+ 'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
243
+ 'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
244
+ }
245
+
246
+ assert_equal('003366', @context['colors.Blue[0]'])
247
+ assert_equal('FF9999', @context['colors.Red[3]'])
248
+ end
249
+
250
+ def test_try_first
251
+ @context['test'] = [1, 2, 3, 4, 5]
252
+
253
+ assert_equal(1, @context['test.first'])
254
+ assert_equal(5, @context['test.last'])
255
+
256
+ @context['test'] = { 'test' => [1, 2, 3, 4, 5] }
257
+
258
+ assert_equal(1, @context['test.test.first'])
259
+ assert_equal(5, @context['test.test.last'])
260
+
261
+ @context['test'] = [1]
262
+ assert_equal(1, @context['test.first'])
263
+ assert_equal(1, @context['test.last'])
264
+ end
265
+
266
+ def test_access_hashes_with_hash_notation
267
+ @context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
268
+ @context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
269
+
270
+ assert_equal(5, @context['products["count"]'])
271
+ assert_equal('deepsnow', @context['products["tags"][0]'])
272
+ assert_equal('deepsnow', @context['products["tags"].first'])
273
+ assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
274
+ assert_equal('element151cm', @context['product["variants"][1]["title"]'])
275
+ assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
276
+ assert_equal('element151cm', @context['product["variants"].last["title"]'])
277
+ end
278
+
279
+ def test_access_variable_with_hash_notation
280
+ @context['foo'] = 'baz'
281
+ @context['bar'] = 'foo'
282
+
283
+ assert_equal('baz', @context['["foo"]'])
284
+ assert_equal('baz', @context['[bar]'])
285
+ end
286
+
287
+ def test_access_hashes_with_hash_access_variables
288
+ @context['var'] = 'tags'
289
+ @context['nested'] = { 'var' => 'tags' }
290
+ @context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
291
+
292
+ assert_equal('deepsnow', @context['products[var].first'])
293
+ assert_equal('freestyle', @context['products[nested.var].last'])
294
+ end
295
+
296
+ def test_hash_notation_only_for_hash_access
297
+ @context['array'] = [1, 2, 3, 4, 5]
298
+ @context['hash'] = { 'first' => 'Hello' }
299
+
300
+ assert_equal(1, @context['array.first'])
301
+ assert_nil(@context['array["first"]'])
302
+ assert_equal('Hello', @context['hash["first"]'])
303
+ end
304
+
305
+ def test_first_can_appear_in_middle_of_callchain
306
+ @context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
307
+
308
+ assert_equal('draft151cm', @context['product.variants[0].title'])
309
+ assert_equal('element151cm', @context['product.variants[1].title'])
310
+ assert_equal('draft151cm', @context['product.variants.first.title'])
311
+ assert_equal('element151cm', @context['product.variants.last.title'])
312
+ end
313
+
314
+ def test_cents
315
+ @context.merge("cents" => HundredCentes.new)
316
+ assert_equal(100, @context['cents'])
317
+ end
318
+
319
+ def test_nested_cents
320
+ @context.merge("cents" => { 'amount' => HundredCentes.new })
321
+ assert_equal(100, @context['cents.amount'])
322
+
323
+ @context.merge("cents" => { 'cents' => { 'amount' => HundredCentes.new } })
324
+ assert_equal(100, @context['cents.cents.amount'])
325
+ end
326
+
327
+ def test_cents_through_drop
328
+ @context.merge("cents" => CentsDrop.new)
329
+ assert_equal(100, @context['cents.amount'])
330
+ end
331
+
332
+ def test_nested_cents_through_drop
333
+ @context.merge("vars" => { "cents" => CentsDrop.new })
334
+ assert_equal(100, @context['vars.cents.amount'])
335
+ end
336
+
337
+ def test_drop_methods_with_question_marks
338
+ @context.merge("cents" => CentsDrop.new)
339
+ assert(@context['cents.non_zero?'])
340
+ end
341
+
342
+ def test_context_from_within_drop
343
+ @context.merge("test" => '123', "vars" => ContextSensitiveDrop.new)
344
+ assert_equal('123', @context['vars.test'])
345
+ end
346
+
347
+ def test_nested_context_from_within_drop
348
+ @context.merge("test" => '123', "vars" => { "local" => ContextSensitiveDrop.new })
349
+ assert_equal('123', @context['vars.local.test'])
350
+ end
351
+
352
+ def test_ranges
353
+ @context.merge("test" => '5')
354
+ assert_equal((1..5), @context['(1..5)'])
355
+ assert_equal((1..5), @context['(1..test)'])
356
+ assert_equal((5..5), @context['(test..test)'])
357
+ end
358
+
359
+ def test_cents_through_drop_nestedly
360
+ @context.merge("cents" => { "cents" => CentsDrop.new })
361
+ assert_equal(100, @context['cents.cents.amount'])
362
+
363
+ @context.merge("cents" => { "cents" => { "cents" => CentsDrop.new } })
364
+ assert_equal(100, @context['cents.cents.cents.amount'])
365
+ end
366
+
367
+ def test_drop_with_variable_called_only_once
368
+ @context['counter'] = CounterDrop.new
369
+
370
+ assert_equal(1, @context['counter.count'])
371
+ assert_equal(2, @context['counter.count'])
372
+ assert_equal(3, @context['counter.count'])
373
+ end
374
+
375
+ def test_drop_with_key_called_only_once
376
+ @context['counter'] = CounterDrop.new
377
+
378
+ assert_equal(1, @context['counter["count"]'])
379
+ assert_equal(2, @context['counter["count"]'])
380
+ assert_equal(3, @context['counter["count"]'])
381
+ end
382
+
383
+ def test_proc_as_variable
384
+ @context['dynamic'] = proc { 'Hello' }
385
+
386
+ assert_equal('Hello', @context['dynamic'])
387
+ end
388
+
389
+ def test_lambda_as_variable
390
+ @context['dynamic'] = proc { 'Hello' }
391
+
392
+ assert_equal('Hello', @context['dynamic'])
393
+ end
394
+
395
+ def test_nested_lambda_as_variable
396
+ @context['dynamic'] = { "lambda" => proc { 'Hello' } }
397
+
398
+ assert_equal('Hello', @context['dynamic.lambda'])
399
+ end
400
+
401
+ def test_array_containing_lambda_as_variable
402
+ @context['dynamic'] = [1, 2, proc { 'Hello' }, 4, 5]
403
+
404
+ assert_equal('Hello', @context['dynamic[2]'])
405
+ end
406
+
407
+ def test_lambda_is_called_once
408
+ @context['callcount'] = proc {
409
+ @global ||= 0
410
+ @global += 1
411
+ @global.to_s
412
+ }
413
+
414
+ assert_equal('1', @context['callcount'])
415
+ assert_equal('1', @context['callcount'])
416
+ assert_equal('1', @context['callcount'])
417
+
418
+ @global = nil
419
+ end
420
+
421
+ def test_nested_lambda_is_called_once
422
+ @context['callcount'] = { "lambda" => proc {
423
+ @global ||= 0
424
+ @global += 1
425
+ @global.to_s
426
+ } }
427
+
428
+ assert_equal('1', @context['callcount.lambda'])
429
+ assert_equal('1', @context['callcount.lambda'])
430
+ assert_equal('1', @context['callcount.lambda'])
431
+
432
+ @global = nil
433
+ end
434
+
435
+ def test_lambda_in_array_is_called_once
436
+ @context['callcount'] = [1, 2, proc {
437
+ @global ||= 0
438
+ @global += 1
439
+ @global.to_s
440
+ }, 4, 5]
441
+
442
+ assert_equal('1', @context['callcount[2]'])
443
+ assert_equal('1', @context['callcount[2]'])
444
+ assert_equal('1', @context['callcount[2]'])
445
+
446
+ @global = nil
447
+ end
448
+
449
+ def test_access_to_context_from_proc
450
+ @context.registers[:magic] = 345392
451
+
452
+ @context['magic'] = proc { @context.registers[:magic] }
453
+
454
+ assert_equal(345392, @context['magic'])
455
+ end
456
+
457
+ def test_to_liquid_and_context_at_first_level
458
+ @context['category'] = Category.new("foobar")
459
+ assert_kind_of(CategoryDrop, @context['category'])
460
+ assert_equal(@context, @context['category'].context)
461
+ end
462
+
463
+ def test_interrupt_avoids_object_allocations
464
+ assert_no_object_allocations do
465
+ @context.interrupt?
466
+ end
467
+ end
468
+
469
+ def test_context_initialization_with_a_proc_in_environment
470
+ contx = Context.new([test: ->(c) { c['poutine'] }], test: :foo)
471
+
472
+ assert(contx)
473
+ assert_nil(contx['poutine'])
474
+ end
475
+
476
+ def test_apply_global_filter
477
+ global_filter_proc = ->(output) { "#{output} filtered" }
478
+
479
+ context = Context.new
480
+ context.global_filter = global_filter_proc
481
+
482
+ assert_equal('hi filtered', context.apply_global_filter('hi'))
483
+ end
484
+
485
+ def test_static_environments_are_read_with_lower_priority_than_environments
486
+ context = Context.build(
487
+ static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' },
488
+ environments: { 'shadowed' => 'dynamic' }
489
+ )
490
+
491
+ assert_equal('dynamic', context['shadowed'])
492
+ assert_equal('static', context['unshadowed'])
493
+ end
494
+
495
+ def test_apply_global_filter_when_no_global_filter_exist
496
+ context = Context.new
497
+ assert_equal('hi', context.apply_global_filter('hi'))
498
+ end
499
+
500
+ def test_new_isolated_subcontext_does_not_inherit_variables
501
+ super_context = Context.new
502
+ super_context['my_variable'] = 'some value'
503
+ subcontext = super_context.new_isolated_subcontext
504
+
505
+ assert_nil(subcontext['my_variable'])
506
+ end
507
+
508
+ def test_new_isolated_subcontext_inherits_static_environment
509
+ super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' })
510
+ subcontext = super_context.new_isolated_subcontext
511
+
512
+ assert_equal('my value', subcontext['my_environment_value'])
513
+ end
514
+
515
+ def test_new_isolated_subcontext_inherits_resource_limits
516
+ resource_limits = ResourceLimits.new({})
517
+ super_context = Context.new({}, {}, {}, false, resource_limits)
518
+ subcontext = super_context.new_isolated_subcontext
519
+ assert_equal(resource_limits, subcontext.resource_limits)
520
+ end
521
+
522
+ def test_new_isolated_subcontext_inherits_exception_renderer
523
+ super_context = Context.new
524
+ super_context.exception_renderer = ->(_e) { 'my exception message' }
525
+ subcontext = super_context.new_isolated_subcontext
526
+ assert_equal('my exception message', subcontext.handle_error(Liquid::Error.new))
527
+ end
528
+
529
+ def test_new_isolated_subcontext_does_not_inherit_non_static_registers
530
+ registers = {
531
+ my_register: :my_value,
532
+ }
533
+ super_context = Context.new({}, {}, StaticRegisters.new(registers))
534
+ super_context.registers[:my_register] = :my_alt_value
535
+ subcontext = super_context.new_isolated_subcontext
536
+ assert_equal(:my_value, subcontext.registers[:my_register])
537
+ end
538
+
539
+ def test_new_isolated_subcontext_inherits_static_registers
540
+ super_context = Context.build(registers: { my_register: :my_value })
541
+ subcontext = super_context.new_isolated_subcontext
542
+ assert_equal(:my_value, subcontext.registers[:my_register])
543
+ end
544
+
545
+ def test_new_isolated_subcontext_registers_do_not_pollute_context
546
+ super_context = Context.build(registers: { my_register: :my_value })
547
+ subcontext = super_context.new_isolated_subcontext
548
+ subcontext.registers[:my_register] = :my_alt_value
549
+ assert_equal(:my_value, super_context.registers[:my_register])
550
+ end
551
+
552
+ def test_new_isolated_subcontext_inherits_filters
553
+ my_filter = Module.new do
554
+ def my_filter(*)
555
+ 'my filter result'
556
+ end
557
+ end
558
+
559
+ super_context = Context.new
560
+ super_context.add_filters([my_filter])
561
+ subcontext = super_context.new_isolated_subcontext
562
+ template = Template.parse('{{ 123 | my_filter }}')
563
+ assert_equal('my filter result', template.render(subcontext))
564
+ end
565
+
566
+ def test_disables_tag_specified
567
+ context = Context.new
568
+ context.with_disabled_tags(%w(foo bar)) do
569
+ assert_equal(true, context.tag_disabled?("foo"))
570
+ assert_equal(true, context.tag_disabled?("bar"))
571
+ assert_equal(false, context.tag_disabled?("unknown"))
572
+ end
573
+ end
574
+
575
+ def test_disables_nested_tags
576
+ context = Context.new
577
+ context.with_disabled_tags(["foo"]) do
578
+ context.with_disabled_tags(["foo"]) do
579
+ assert_equal(true, context.tag_disabled?("foo"))
580
+ assert_equal(false, context.tag_disabled?("bar"))
581
+ end
582
+ context.with_disabled_tags(["bar"]) do
583
+ assert_equal(true, context.tag_disabled?("foo"))
584
+ assert_equal(true, context.tag_disabled?("bar"))
585
+ context.with_disabled_tags(["foo"]) do
586
+ assert_equal(true, context.tag_disabled?("foo"))
587
+ assert_equal(true, context.tag_disabled?("bar"))
588
+ end
589
+ end
590
+ assert_equal(true, context.tag_disabled?("foo"))
591
+ assert_equal(false, context.tag_disabled?("bar"))
592
+ end
593
+ end
594
+
6
595
  def test_override_global_filter
7
596
  global = Module.new do
8
597
  def notice(output)
@@ -17,16 +606,30 @@ class ContextTest < Minitest::Test
17
606
  end
18
607
 
19
608
  with_global_filter(global) do
20
- assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
21
- assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local])
609
+ assert_equal('Global test', Template.parse("{{'test' | notice }}").render!)
610
+ assert_equal('Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local]))
22
611
  end
23
612
  end
24
613
 
25
614
  def test_has_key_will_not_add_an_error_for_missing_keys
26
- with_error_mode :strict do
615
+ with_error_mode(:strict) do
27
616
  context = Context.new
28
617
  context.key?('unknown')
29
- assert_empty context.errors
618
+ assert_empty(context.errors)
30
619
  end
31
620
  end
32
- end
621
+
622
+ private
623
+
624
+ def assert_no_object_allocations
625
+ unless RUBY_ENGINE == 'ruby'
626
+ skip("stackprof needed to count object allocations")
627
+ end
628
+ require 'stackprof'
629
+
630
+ profile = StackProf.run(mode: :object) do
631
+ yield
632
+ end
633
+ assert_equal(0, profile[:samples])
634
+ end
635
+ end # ContextTest