liquid 2.6.1 → 4.0.3

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 (130) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +194 -29
  3. data/{MIT-LICENSE → LICENSE} +0 -0
  4. data/README.md +60 -2
  5. data/lib/liquid.rb +25 -14
  6. data/lib/liquid/block.rb +47 -96
  7. data/lib/liquid/block_body.rb +143 -0
  8. data/lib/liquid/condition.rb +70 -39
  9. data/lib/liquid/context.rb +116 -157
  10. data/lib/liquid/document.rb +19 -9
  11. data/lib/liquid/drop.rb +31 -14
  12. data/lib/liquid/errors.rb +54 -10
  13. data/lib/liquid/expression.rb +49 -0
  14. data/lib/liquid/extensions.rb +19 -7
  15. data/lib/liquid/file_system.rb +25 -14
  16. data/lib/liquid/forloop_drop.rb +42 -0
  17. data/lib/liquid/i18n.rb +39 -0
  18. data/lib/liquid/interrupts.rb +2 -3
  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 +311 -77
  30. data/lib/liquid/strainer.rb +39 -26
  31. data/lib/liquid/tablerowloop_drop.rb +62 -0
  32. data/lib/liquid/tag.rb +28 -11
  33. data/lib/liquid/tags/assign.rb +34 -10
  34. data/lib/liquid/tags/break.rb +1 -4
  35. data/lib/liquid/tags/capture.rb +11 -9
  36. data/lib/liquid/tags/case.rb +37 -22
  37. data/lib/liquid/tags/comment.rb +10 -3
  38. data/lib/liquid/tags/continue.rb +1 -4
  39. data/lib/liquid/tags/cycle.rb +20 -14
  40. data/lib/liquid/tags/decrement.rb +4 -8
  41. data/lib/liquid/tags/for.rb +121 -60
  42. data/lib/liquid/tags/if.rb +73 -30
  43. data/lib/liquid/tags/ifchanged.rb +3 -5
  44. data/lib/liquid/tags/include.rb +77 -46
  45. data/lib/liquid/tags/increment.rb +4 -8
  46. data/lib/liquid/tags/raw.rb +35 -10
  47. data/lib/liquid/tags/table_row.rb +62 -0
  48. data/lib/liquid/tags/unless.rb +6 -9
  49. data/lib/liquid/template.rb +130 -32
  50. data/lib/liquid/tokenizer.rb +31 -0
  51. data/lib/liquid/truffle.rb +5 -0
  52. data/lib/liquid/utils.rb +57 -4
  53. data/lib/liquid/variable.rb +121 -30
  54. data/lib/liquid/variable_lookup.rb +88 -0
  55. data/lib/liquid/version.rb +2 -1
  56. data/test/fixtures/en_locale.yml +9 -0
  57. data/test/integration/assign_test.rb +48 -0
  58. data/test/integration/blank_test.rb +106 -0
  59. data/test/integration/block_test.rb +12 -0
  60. data/test/{liquid → integration}/capture_test.rb +13 -3
  61. data/test/integration/context_test.rb +32 -0
  62. data/test/integration/document_test.rb +19 -0
  63. data/test/integration/drop_test.rb +273 -0
  64. data/test/integration/error_handling_test.rb +260 -0
  65. data/test/integration/filter_test.rb +178 -0
  66. data/test/integration/hash_ordering_test.rb +23 -0
  67. data/test/integration/output_test.rb +123 -0
  68. data/test/integration/parse_tree_visitor_test.rb +247 -0
  69. data/test/integration/parsing_quirks_test.rb +122 -0
  70. data/test/integration/render_profiling_test.rb +154 -0
  71. data/test/integration/security_test.rb +80 -0
  72. data/test/integration/standard_filter_test.rb +776 -0
  73. data/test/{liquid → integration}/tags/break_tag_test.rb +2 -3
  74. data/test/{liquid → integration}/tags/continue_tag_test.rb +1 -2
  75. data/test/integration/tags/for_tag_test.rb +410 -0
  76. data/test/integration/tags/if_else_tag_test.rb +188 -0
  77. data/test/integration/tags/include_tag_test.rb +253 -0
  78. data/test/integration/tags/increment_tag_test.rb +23 -0
  79. data/test/{liquid → integration}/tags/raw_tag_test.rb +9 -2
  80. data/test/integration/tags/standard_tag_test.rb +296 -0
  81. data/test/integration/tags/statements_test.rb +111 -0
  82. data/test/{liquid/tags/html_tag_test.rb → integration/tags/table_row_test.rb} +25 -24
  83. data/test/integration/tags/unless_else_tag_test.rb +26 -0
  84. data/test/integration/template_test.rb +332 -0
  85. data/test/integration/trim_mode_test.rb +529 -0
  86. data/test/integration/variable_test.rb +96 -0
  87. data/test/test_helper.rb +106 -19
  88. data/test/truffle/truffle_test.rb +9 -0
  89. data/test/{liquid/block_test.rb → unit/block_unit_test.rb} +9 -9
  90. data/test/unit/condition_unit_test.rb +166 -0
  91. data/test/{liquid/context_test.rb → unit/context_unit_test.rb} +85 -74
  92. data/test/unit/file_system_unit_test.rb +35 -0
  93. data/test/unit/i18n_unit_test.rb +37 -0
  94. data/test/unit/lexer_unit_test.rb +51 -0
  95. data/test/unit/parser_unit_test.rb +82 -0
  96. data/test/{liquid/regexp_test.rb → unit/regexp_unit_test.rb} +4 -4
  97. data/test/unit/strainer_unit_test.rb +164 -0
  98. data/test/unit/tag_unit_test.rb +21 -0
  99. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  100. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  101. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  102. data/test/unit/template_unit_test.rb +78 -0
  103. data/test/unit/tokenizer_unit_test.rb +55 -0
  104. data/test/unit/variable_unit_test.rb +162 -0
  105. metadata +157 -77
  106. data/lib/extras/liquid_view.rb +0 -51
  107. data/lib/liquid/htmltags.rb +0 -74
  108. data/lib/liquid/module_ex.rb +0 -62
  109. data/test/liquid/assign_test.rb +0 -21
  110. data/test/liquid/condition_test.rb +0 -127
  111. data/test/liquid/drop_test.rb +0 -180
  112. data/test/liquid/error_handling_test.rb +0 -81
  113. data/test/liquid/file_system_test.rb +0 -29
  114. data/test/liquid/filter_test.rb +0 -125
  115. data/test/liquid/hash_ordering_test.rb +0 -25
  116. data/test/liquid/module_ex_test.rb +0 -87
  117. data/test/liquid/output_test.rb +0 -116
  118. data/test/liquid/parsing_quirks_test.rb +0 -52
  119. data/test/liquid/security_test.rb +0 -64
  120. data/test/liquid/standard_filter_test.rb +0 -251
  121. data/test/liquid/strainer_test.rb +0 -52
  122. data/test/liquid/tags/for_tag_test.rb +0 -297
  123. data/test/liquid/tags/if_else_tag_test.rb +0 -166
  124. data/test/liquid/tags/include_tag_test.rb +0 -166
  125. data/test/liquid/tags/increment_tag_test.rb +0 -24
  126. data/test/liquid/tags/standard_tag_test.rb +0 -295
  127. data/test/liquid/tags/statements_test.rb +0 -134
  128. data/test/liquid/tags/unless_else_tag_test.rb +0 -26
  129. data/test/liquid/template_test.rb +0 -146
  130. data/test/liquid/variable_test.rb +0 -186
@@ -0,0 +1,143 @@
1
+ module Liquid
2
+ class BlockBody
3
+ FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
4
+ ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
5
+ WhitespaceOrNothing = /\A\s*\z/
6
+ TAGSTART = "{%".freeze
7
+ VARSTART = "{{".freeze
8
+
9
+ attr_reader :nodelist
10
+
11
+ def initialize
12
+ @nodelist = []
13
+ @blank = true
14
+ end
15
+
16
+ def parse(tokenizer, parse_context)
17
+ parse_context.line_number = tokenizer.line_number
18
+ while token = tokenizer.shift
19
+ next if token.empty?
20
+ case
21
+ when token.start_with?(TAGSTART)
22
+ whitespace_handler(token, parse_context)
23
+ unless token =~ FullToken
24
+ raise_missing_tag_terminator(token, parse_context)
25
+ end
26
+ tag_name = $1
27
+ markup = $2
28
+ # fetch the tag from registered blocks
29
+ unless tag = registered_tags[tag_name]
30
+ # end parsing if we reach an unknown tag and let the caller decide
31
+ # determine how to proceed
32
+ return yield tag_name, markup
33
+ end
34
+ new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
35
+ @blank &&= new_tag.blank?
36
+ @nodelist << new_tag
37
+ when token.start_with?(VARSTART)
38
+ whitespace_handler(token, parse_context)
39
+ @nodelist << create_variable(token, parse_context)
40
+ @blank = false
41
+ else
42
+ if parse_context.trim_whitespace
43
+ token.lstrip!
44
+ end
45
+ parse_context.trim_whitespace = false
46
+ @nodelist << token
47
+ @blank &&= !!(token =~ WhitespaceOrNothing)
48
+ end
49
+ parse_context.line_number = tokenizer.line_number
50
+ end
51
+
52
+ yield nil, nil
53
+ end
54
+
55
+ def whitespace_handler(token, parse_context)
56
+ if token[2] == WhitespaceControl
57
+ previous_token = @nodelist.last
58
+ if previous_token.is_a? String
59
+ previous_token.rstrip!
60
+ end
61
+ end
62
+ parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
63
+ end
64
+
65
+ def blank?
66
+ @blank
67
+ end
68
+
69
+ def render(context)
70
+ output = []
71
+ context.resource_limits.render_score += @nodelist.length
72
+
73
+ idx = 0
74
+ while node = @nodelist[idx]
75
+ case node
76
+ when String
77
+ check_resources(context, node)
78
+ output << node
79
+ when Variable
80
+ render_node_to_output(node, output, context)
81
+ when Block
82
+ render_node_to_output(node, output, context, node.blank?)
83
+ break if context.interrupt? # might have happened in a for-block
84
+ when Continue, Break
85
+ # If we get an Interrupt that means the block must stop processing. An
86
+ # Interrupt is any command that stops block execution such as {% break %}
87
+ # or {% continue %}
88
+ context.push_interrupt(node.interrupt)
89
+ break
90
+ else # Other non-Block tags
91
+ render_node_to_output(node, output, context)
92
+ break if context.interrupt? # might have happened through an include
93
+ end
94
+ idx += 1
95
+ end
96
+
97
+ output.join
98
+ end
99
+
100
+ private
101
+
102
+ def render_node_to_output(node, output, context, skip_output = false)
103
+ node_output = node.render(context)
104
+ node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
105
+ check_resources(context, node_output)
106
+ output << node_output unless skip_output
107
+ rescue MemoryError => e
108
+ raise e
109
+ rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
110
+ context.handle_error(e, node.line_number)
111
+ output << nil
112
+ rescue ::StandardError => e
113
+ line_number = node.is_a?(String) ? nil : node.line_number
114
+ output << context.handle_error(e, line_number)
115
+ end
116
+
117
+ def check_resources(context, node_output)
118
+ context.resource_limits.render_length += node_output.length
119
+ return unless context.resource_limits.reached?
120
+ raise MemoryError.new("Memory limits exceeded".freeze)
121
+ end
122
+
123
+ def create_variable(token, parse_context)
124
+ token.scan(ContentOfVariable) do |content|
125
+ markup = content.first
126
+ return Variable.new(markup, parse_context)
127
+ end
128
+ raise_missing_variable_terminator(token, parse_context)
129
+ end
130
+
131
+ def raise_missing_tag_terminator(token, parse_context)
132
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
133
+ end
134
+
135
+ def raise_missing_variable_terminator(token, parse_context)
136
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
137
+ end
138
+
139
+ def registered_tags
140
+ Template.tags
141
+ end
142
+ end
143
+ end
@@ -3,53 +3,70 @@ module Liquid
3
3
  #
4
4
  # Example:
5
5
  #
6
- # c = Condition.new('1', '==', '1')
6
+ # c = Condition.new(1, '==', 1)
7
7
  # c.evaluate #=> true
8
8
  #
9
9
  class Condition #:nodoc:
10
10
  @@operators = {
11
- '==' => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
12
- '!=' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
13
- '<>' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
14
- '<' => :<,
15
- '>' => :>,
16
- '>=' => :>=,
17
- '<=' => :<=,
18
- 'contains' => lambda { |cond, left, right| left && right ? left.include?(right) : false }
11
+ '=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
12
+ '!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
13
+ '<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
14
+ '<'.freeze => :<,
15
+ '>'.freeze => :>,
16
+ '>='.freeze => :>=,
17
+ '<='.freeze => :<=,
18
+ 'contains'.freeze => lambda do |cond, left, right|
19
+ if left && right && left.respond_to?(:include?)
20
+ right = right.to_s if left.is_a?(String)
21
+ left.include?(right)
22
+ else
23
+ false
24
+ end
25
+ end
19
26
  }
20
27
 
21
28
  def self.operators
22
29
  @@operators
23
30
  end
24
31
 
25
- attr_reader :attachment
32
+ attr_reader :attachment, :child_condition
26
33
  attr_accessor :left, :operator, :right
27
34
 
28
35
  def initialize(left = nil, operator = nil, right = nil)
29
- @left, @operator, @right = left, operator, right
36
+ @left = left
37
+ @operator = operator
38
+ @right = right
30
39
  @child_relation = nil
31
40
  @child_condition = nil
32
41
  end
33
42
 
34
43
  def evaluate(context = Context.new)
35
- result = interpret_condition(left, right, operator, context)
36
-
37
- case @child_relation
38
- when :or
39
- result || @child_condition.evaluate(context)
40
- when :and
41
- result && @child_condition.evaluate(context)
42
- else
43
- result
44
+ condition = self
45
+ result = nil
46
+ loop do
47
+ result = interpret_condition(condition.left, condition.right, condition.operator, context)
48
+
49
+ case condition.child_relation
50
+ when :or
51
+ break if result
52
+ when :and
53
+ break unless result
54
+ else
55
+ break
56
+ end
57
+ condition = condition.child_condition
44
58
  end
59
+ result
45
60
  end
46
61
 
47
62
  def or(condition)
48
- @child_relation, @child_condition = :or, condition
63
+ @child_relation = :or
64
+ @child_condition = condition
49
65
  end
50
66
 
51
67
  def and(condition)
52
- @child_relation, @child_condition = :and, condition
68
+ @child_relation = :and
69
+ @child_condition = condition
53
70
  end
54
71
 
55
72
  def attach(attachment)
@@ -61,23 +78,27 @@ module Liquid
61
78
  end
62
79
 
63
80
  def inspect
64
- "#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
81
+ "#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
65
82
  end
66
83
 
84
+ protected
85
+
86
+ attr_reader :child_relation
87
+
67
88
  private
68
89
 
69
90
  def equal_variables(left, right)
70
- if left.is_a?(Symbol)
71
- if right.respond_to?(left)
72
- return right.send(left.to_s)
91
+ if left.is_a?(Liquid::Expression::MethodLiteral)
92
+ if right.respond_to?(left.method_name)
93
+ return right.send(left.method_name)
73
94
  else
74
95
  return nil
75
96
  end
76
97
  end
77
98
 
78
- if right.is_a?(Symbol)
79
- if left.respond_to?(right)
80
- return left.send(right.to_s)
99
+ if right.is_a?(Liquid::Expression::MethodLiteral)
100
+ if left.respond_to?(right.method_name)
101
+ return left.send(right.method_name)
81
102
  else
82
103
  return nil
83
104
  end
@@ -90,31 +111,41 @@ module Liquid
90
111
  # If the operator is empty this means that the decision statement is just
91
112
  # a single variable. We can just poll this variable from the context and
92
113
  # return this as the result.
93
- return context[left] if op == nil
114
+ return context.evaluate(left) if op.nil?
94
115
 
95
- left, right = context[left], context[right]
116
+ left = context.evaluate(left)
117
+ right = context.evaluate(right)
96
118
 
97
- operation = self.class.operators[op] || raise(ArgumentError.new("Unknown operator #{op}"))
119
+ operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
98
120
 
99
121
  if operation.respond_to?(:call)
100
122
  operation.call(self, left, right)
101
- elsif left.respond_to?(operation) and right.respond_to?(operation)
102
- left.send(operation, right)
103
- else
104
- nil
123
+ elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
124
+ begin
125
+ left.send(operation, right)
126
+ rescue ::ArgumentError => e
127
+ raise Liquid::ArgumentError.new(e.message)
128
+ end
105
129
  end
106
130
  end
107
- end
108
131
 
132
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
133
+ def children
134
+ [
135
+ @node.left, @node.right,
136
+ @node.child_condition, @node.attachment
137
+ ].compact
138
+ end
139
+ end
140
+ end
109
141
 
110
142
  class ElseCondition < Condition
111
143
  def else?
112
144
  true
113
145
  end
114
146
 
115
- def evaluate(context)
147
+ def evaluate(_context)
116
148
  true
117
149
  end
118
150
  end
119
-
120
151
  end
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # Context keeps the variable stack and resolves variables, as well as keywords
4
3
  #
5
4
  # context['variable'] = 'testing'
@@ -14,27 +13,36 @@ module Liquid
14
13
  # context['bob'] #=> nil class Context
15
14
  class Context
16
15
  attr_reader :scopes, :errors, :registers, :environments, :resource_limits
17
-
18
- def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
19
- @environments = [environments].flatten
20
- @scopes = [(outer_scope || {})]
21
- @registers = registers
22
- @errors = []
23
- @rethrow_errors = rethrow_errors
24
- @resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
16
+ attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
17
+
18
+ def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
19
+ @environments = [environments].flatten
20
+ @scopes = [(outer_scope || {})]
21
+ @registers = registers
22
+ @errors = []
23
+ @partial = false
24
+ @strict_variables = false
25
+ @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
25
26
  squash_instance_assigns_with_environments
26
27
 
28
+ @this_stack_used = false
29
+
30
+ self.exception_renderer = Template.default_exception_renderer
31
+ if rethrow_errors
32
+ self.exception_renderer = ->(e) { raise }
33
+ end
34
+
27
35
  @interrupts = []
36
+ @filters = []
37
+ @global_filter = nil
28
38
  end
29
39
 
30
- def resource_limits_reached?
31
- (@resource_limits[:render_length_limit] && @resource_limits[:render_length_current] > @resource_limits[:render_length_limit]) ||
32
- (@resource_limits[:render_score_limit] && @resource_limits[:render_score_current] > @resource_limits[:render_score_limit] ) ||
33
- (@resource_limits[:assign_score_limit] && @resource_limits[:assign_score_current] > @resource_limits[:assign_score_limit] )
40
+ def warnings
41
+ @warnings ||= []
34
42
  end
35
43
 
36
44
  def strainer
37
- @strainer ||= Strainer.create(self)
45
+ @strainer ||= Strainer.create(self, @filters)
38
46
  end
39
47
 
40
48
  # Adds filters to this context.
@@ -43,17 +51,17 @@ module Liquid
43
51
  # for that
44
52
  def add_filters(filters)
45
53
  filters = [filters].flatten.compact
54
+ @filters += filters
55
+ @strainer = nil
56
+ end
46
57
 
47
- filters.each do |f|
48
- raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
49
- Strainer.add_known_filter(f)
50
- strainer.extend(f)
51
- end
58
+ def apply_global_filter(obj)
59
+ global_filter.nil? ? obj : global_filter.call(obj)
52
60
  end
53
61
 
54
62
  # are there any not handled interrupts?
55
- def has_interrupt?
56
- @interrupts.any?
63
+ def interrupt?
64
+ !@interrupts.empty?
57
65
  end
58
66
 
59
67
  # push an interrupt to the stack. this interrupt is considered not handled.
@@ -66,26 +74,22 @@ module Liquid
66
74
  @interrupts.pop
67
75
  end
68
76
 
69
- def handle_error(e)
77
+ def handle_error(e, line_number = nil)
78
+ e = internal_error unless e.is_a?(Liquid::Error)
79
+ e.template_name ||= template_name
80
+ e.line_number ||= line_number
70
81
  errors.push(e)
71
- raise if @rethrow_errors
72
-
73
- case e
74
- when SyntaxError
75
- "Liquid syntax error: #{e.message}"
76
- else
77
- "Liquid error: #{e.message}"
78
- end
82
+ exception_renderer.call(e).to_s
79
83
  end
80
84
 
81
85
  def invoke(method, *args)
82
- strainer.invoke(method, *args)
86
+ strainer.invoke(method, *args).to_liquid
83
87
  end
84
88
 
85
89
  # Push new local scope on the stack. use <tt>Context#stack</tt> instead
86
- def push(new_scope={})
90
+ def push(new_scope = {})
87
91
  @scopes.unshift(new_scope)
88
- raise StackLevelError, "Nesting too deep" if @scopes.length > 100
92
+ raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH
89
93
  end
90
94
 
91
95
  # Merge a hash of variables in the current local scope
@@ -107,11 +111,19 @@ module Liquid
107
111
  # end
108
112
  #
109
113
  # context['var] #=> nil
110
- def stack(new_scope={})
111
- push(new_scope)
114
+ def stack(new_scope = nil)
115
+ old_stack_used = @this_stack_used
116
+ if new_scope
117
+ push(new_scope)
118
+ @this_stack_used = true
119
+ else
120
+ @this_stack_used = false
121
+ end
122
+
112
123
  yield
113
124
  ensure
114
- pop
125
+ pop if @this_stack_used
126
+ @this_stack_used = old_stack_used
115
127
  end
116
128
 
117
129
  def clear_instance_assigns
@@ -120,148 +132,95 @@ module Liquid
120
132
 
121
133
  # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
122
134
  def []=(key, value)
135
+ unless @this_stack_used
136
+ @this_stack_used = true
137
+ push({})
138
+ end
123
139
  @scopes[0][key] = value
124
140
  end
125
141
 
126
- def [](key)
127
- resolve(key)
142
+ # Look up variable, either resolve directly after considering the name. We can directly handle
143
+ # Strings, digits, floats and booleans (true,false).
144
+ # If no match is made we lookup the variable in the current scope and
145
+ # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
146
+ # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
147
+ #
148
+ # Example:
149
+ # products == empty #=> products.empty?
150
+ def [](expression)
151
+ evaluate(Expression.parse(expression))
128
152
  end
129
153
 
130
- def has_key?(key)
131
- resolve(key) != nil
154
+ def key?(key)
155
+ self[key] != nil
132
156
  end
133
157
 
134
- private
135
- LITERALS = {
136
- nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
137
- 'true' => true,
138
- 'false' => false,
139
- 'blank' => :blank?,
140
- 'empty' => :empty?
141
- }
142
-
143
- # Look up variable, either resolve directly after considering the name. We can directly handle
144
- # Strings, digits, floats and booleans (true,false).
145
- # If no match is made we lookup the variable in the current scope and
146
- # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
147
- # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
148
- #
149
- # Example:
150
- # products == empty #=> products.empty?
151
- def resolve(key)
152
- if LITERALS.key?(key)
153
- LITERALS[key]
154
- else
155
- case key
156
- when /^'(.*)'$/ # Single quoted strings
157
- $1
158
- when /^"(.*)"$/ # Double quoted strings
159
- $1
160
- when /^(-?\d+)$/ # Integer and floats
161
- $1.to_i
162
- when /^\((\S+)\.\.(\S+)\)$/ # Ranges
163
- (resolve($1).to_i..resolve($2).to_i)
164
- when /^(-?\d[\d\.]+)$/ # Floats
165
- $1.to_f
166
- else
167
- variable(key)
168
- end
169
- end
170
- end
171
-
172
- # Fetches an object starting at the local scope and then moving up the hierachy
173
- def find_variable(key)
174
- scope = @scopes.find { |s| s.has_key?(key) }
175
- variable = nil
176
-
177
- if scope.nil?
178
- @environments.each do |e|
179
- if variable = lookup_and_evaluate(e, key)
180
- scope = e
181
- break
182
- end
183
- end
184
- end
185
-
186
- scope ||= @environments.last || @scopes.last
187
- variable ||= lookup_and_evaluate(scope, key)
188
-
189
- variable = variable.to_liquid
190
- variable.context = self if variable.respond_to?(:context=)
191
-
192
- return variable
193
- end
158
+ def evaluate(object)
159
+ object.respond_to?(:evaluate) ? object.evaluate(self) : object
160
+ end
194
161
 
195
- # Resolves namespaced queries gracefully.
196
- #
197
- # Example
198
- # @context['hash'] = {"name" => 'tobi'}
199
- # assert_equal 'tobi', @context['hash.name']
200
- # assert_equal 'tobi', @context['hash["name"]']
201
- def variable(markup)
202
- parts = markup.scan(VariableParser)
203
- square_bracketed = /^\[(.*)\]$/
162
+ # Fetches an object starting at the local scope and then moving up the hierachy
163
+ def find_variable(key, raise_on_not_found: true)
164
+ # This was changed from find() to find_index() because this is a very hot
165
+ # path and find_index() is optimized in MRI to reduce object allocation
166
+ index = @scopes.find_index { |s| s.key?(key) }
167
+ scope = @scopes[index] if index
204
168
 
205
- first_part = parts.shift
169
+ variable = nil
206
170
 
207
- if first_part =~ square_bracketed
208
- first_part = resolve($1)
171
+ if scope.nil?
172
+ @environments.each do |e|
173
+ variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
174
+ # When lookup returned a value OR there is no value but the lookup also did not raise
175
+ # then it is the value we are looking for.
176
+ if !variable.nil? || @strict_variables && raise_on_not_found
177
+ scope = e
178
+ break
179
+ end
209
180
  end
181
+ end
210
182
 
211
- if object = find_variable(first_part)
212
-
213
- parts.each do |part|
214
- part = resolve($1) if part_resolved = (part =~ square_bracketed)
183
+ scope ||= @environments.last || @scopes.last
184
+ variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
215
185
 
216
- # If object is a hash- or array-like object we look for the
217
- # presence of the key and if its available we return it
218
- if object.respond_to?(:[]) and
219
- ((object.respond_to?(:has_key?) and object.has_key?(part)) or
220
- (object.respond_to?(:fetch) and part.is_a?(Integer)))
186
+ variable = variable.to_liquid
187
+ variable.context = self if variable.respond_to?(:context=)
221
188
 
222
- # if its a proc we will replace the entry with the proc
223
- res = lookup_and_evaluate(object, part)
224
- object = res.to_liquid
189
+ variable
190
+ end
225
191
 
226
- # Some special cases. If the part wasn't in square brackets and
227
- # no key with the same name was found we interpret following calls
228
- # as commands and call them on the current object
229
- elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
192
+ def lookup_and_evaluate(obj, key, raise_on_not_found: true)
193
+ if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
194
+ raise Liquid::UndefinedVariable, "undefined variable #{key}"
195
+ end
230
196
 
231
- object = object.send(part.intern).to_liquid
197
+ value = obj[key]
232
198
 
233
- # No key was present with the desired value and it wasn't one of the directly supported
234
- # keywords either. The only thing we got left is to return nil
235
- else
236
- return nil
237
- end
199
+ if value.is_a?(Proc) && obj.respond_to?(:[]=)
200
+ obj[key] = (value.arity == 0) ? value.call : value.call(self)
201
+ else
202
+ value
203
+ end
204
+ end
238
205
 
239
- # If we are dealing with a drop here we have to
240
- object.context = self if object.respond_to?(:context=)
241
- end
242
- end
206
+ private
243
207
 
244
- object
245
- end # variable
208
+ def internal_error
209
+ # raise and catch to set backtrace and cause on exception
210
+ raise Liquid::InternalError, 'internal'
211
+ rescue Liquid::InternalError => exc
212
+ exc
213
+ end
246
214
 
247
- def lookup_and_evaluate(obj, key)
248
- if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
249
- obj[key] = (value.arity == 0) ? value.call : value.call(self)
250
- else
251
- value
252
- end
253
- end # lookup_and_evaluate
254
-
255
- def squash_instance_assigns_with_environments
256
- @scopes.last.each_key do |k|
257
- @environments.each do |env|
258
- if env.has_key?(k)
259
- scopes.last[k] = lookup_and_evaluate(env, k)
260
- break
261
- end
215
+ def squash_instance_assigns_with_environments
216
+ @scopes.last.each_key do |k|
217
+ @environments.each do |env|
218
+ if env.key?(k)
219
+ scopes.last[k] = lookup_and_evaluate(env, k)
220
+ break
262
221
  end
263
222
  end
264
- end # squash_instance_assigns_with_environments
223
+ end
224
+ end # squash_instance_assigns_with_environments
265
225
  end # Context
266
-
267
226
  end # Liquid