liquid 2.6.1 → 4.0.3

Sign up to get free protection for your applications and to get access to all the features.
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