liquid 3.0.6 → 4.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +154 -58
  3. data/{MIT-LICENSE → LICENSE} +0 -0
  4. data/README.md +33 -0
  5. data/lib/liquid/block.rb +42 -125
  6. data/lib/liquid/block_body.rb +99 -79
  7. data/lib/liquid/condition.rb +52 -32
  8. data/lib/liquid/context.rb +57 -51
  9. data/lib/liquid/document.rb +19 -9
  10. data/lib/liquid/drop.rb +17 -16
  11. data/lib/liquid/errors.rb +20 -24
  12. data/lib/liquid/expression.rb +26 -10
  13. data/lib/liquid/extensions.rb +19 -7
  14. data/lib/liquid/file_system.rb +11 -11
  15. data/lib/liquid/forloop_drop.rb +42 -0
  16. data/lib/liquid/i18n.rb +6 -6
  17. data/lib/liquid/interrupts.rb +1 -2
  18. data/lib/liquid/lexer.rb +12 -8
  19. data/lib/liquid/locales/en.yml +6 -2
  20. data/lib/liquid/parse_context.rb +38 -0
  21. data/lib/liquid/parse_tree_visitor.rb +42 -0
  22. data/lib/liquid/parser_switching.rb +4 -4
  23. data/lib/liquid/profiler/hooks.rb +7 -7
  24. data/lib/liquid/profiler.rb +18 -19
  25. data/lib/liquid/range_lookup.rb +16 -1
  26. data/lib/liquid/resource_limits.rb +23 -0
  27. data/lib/liquid/standardfilters.rb +207 -61
  28. data/lib/liquid/strainer.rb +15 -8
  29. data/lib/liquid/tablerowloop_drop.rb +62 -0
  30. data/lib/liquid/tag.rb +9 -8
  31. data/lib/liquid/tags/assign.rb +25 -4
  32. data/lib/liquid/tags/break.rb +0 -3
  33. data/lib/liquid/tags/capture.rb +1 -1
  34. data/lib/liquid/tags/case.rb +27 -12
  35. data/lib/liquid/tags/comment.rb +2 -2
  36. data/lib/liquid/tags/cycle.rb +16 -8
  37. data/lib/liquid/tags/decrement.rb +1 -4
  38. data/lib/liquid/tags/for.rb +103 -75
  39. data/lib/liquid/tags/if.rb +60 -44
  40. data/lib/liquid/tags/ifchanged.rb +0 -2
  41. data/lib/liquid/tags/include.rb +71 -51
  42. data/lib/liquid/tags/raw.rb +32 -4
  43. data/lib/liquid/tags/table_row.rb +21 -31
  44. data/lib/liquid/tags/unless.rb +3 -4
  45. data/lib/liquid/template.rb +42 -54
  46. data/lib/liquid/tokenizer.rb +31 -0
  47. data/lib/liquid/truffle.rb +5 -0
  48. data/lib/liquid/utils.rb +52 -8
  49. data/lib/liquid/variable.rb +59 -46
  50. data/lib/liquid/variable_lookup.rb +14 -6
  51. data/lib/liquid/version.rb +2 -1
  52. data/lib/liquid.rb +10 -7
  53. data/test/integration/assign_test.rb +8 -8
  54. data/test/integration/blank_test.rb +14 -14
  55. data/test/integration/block_test.rb +12 -0
  56. data/test/integration/context_test.rb +2 -2
  57. data/test/integration/document_test.rb +19 -0
  58. data/test/integration/drop_test.rb +42 -40
  59. data/test/integration/error_handling_test.rb +96 -43
  60. data/test/integration/filter_test.rb +60 -20
  61. data/test/integration/hash_ordering_test.rb +9 -9
  62. data/test/integration/output_test.rb +26 -27
  63. data/test/integration/parse_tree_visitor_test.rb +247 -0
  64. data/test/integration/parsing_quirks_test.rb +19 -13
  65. data/test/integration/render_profiling_test.rb +20 -20
  66. data/test/integration/security_test.rb +23 -7
  67. data/test/integration/standard_filter_test.rb +426 -46
  68. data/test/integration/tags/break_tag_test.rb +1 -2
  69. data/test/integration/tags/continue_tag_test.rb +0 -1
  70. data/test/integration/tags/for_tag_test.rb +135 -100
  71. data/test/integration/tags/if_else_tag_test.rb +75 -77
  72. data/test/integration/tags/include_tag_test.rb +50 -31
  73. data/test/integration/tags/increment_tag_test.rb +10 -11
  74. data/test/integration/tags/raw_tag_test.rb +7 -1
  75. data/test/integration/tags/standard_tag_test.rb +121 -122
  76. data/test/integration/tags/statements_test.rb +3 -5
  77. data/test/integration/tags/table_row_test.rb +20 -19
  78. data/test/integration/tags/unless_else_tag_test.rb +6 -6
  79. data/test/integration/template_test.rb +199 -49
  80. data/test/integration/trim_mode_test.rb +529 -0
  81. data/test/integration/variable_test.rb +27 -13
  82. data/test/test_helper.rb +33 -6
  83. data/test/truffle/truffle_test.rb +9 -0
  84. data/test/unit/block_unit_test.rb +8 -5
  85. data/test/unit/condition_unit_test.rb +94 -77
  86. data/test/unit/context_unit_test.rb +69 -72
  87. data/test/unit/file_system_unit_test.rb +3 -3
  88. data/test/unit/i18n_unit_test.rb +2 -2
  89. data/test/unit/lexer_unit_test.rb +12 -9
  90. data/test/unit/parser_unit_test.rb +2 -2
  91. data/test/unit/regexp_unit_test.rb +1 -1
  92. data/test/unit/strainer_unit_test.rb +96 -1
  93. data/test/unit/tag_unit_test.rb +7 -2
  94. data/test/unit/tags/case_tag_unit_test.rb +1 -1
  95. data/test/unit/tags/for_tag_unit_test.rb +2 -2
  96. data/test/unit/tags/if_tag_unit_test.rb +1 -1
  97. data/test/unit/template_unit_test.rb +14 -5
  98. data/test/unit/tokenizer_unit_test.rb +24 -7
  99. data/test/unit/variable_unit_test.rb +60 -43
  100. metadata +62 -50
  101. data/lib/liquid/module_ex.rb +0 -62
  102. data/lib/liquid/token.rb +0 -18
  103. data/test/unit/module_ex_unit_test.rb +0 -87
@@ -1,7 +1,8 @@
1
1
  module Liquid
2
2
  class BlockBody
3
- FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
4
- ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
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/
5
6
  TAGSTART = "{%".freeze
6
7
  VARSTART = "{{".freeze
7
8
 
@@ -12,89 +13,85 @@ module Liquid
12
13
  @blank = true
13
14
  end
14
15
 
15
- def parse(tokens, options)
16
- while token = tokens.shift
17
- begin
18
- unless token.empty?
19
- case
20
- when token.start_with?(TAGSTART)
21
- if token =~ FullToken
22
- tag_name = $1
23
- markup = $2
24
- # fetch the tag from registered blocks
25
- if tag = Template.tags[tag_name]
26
- markup = token.child(markup) if token.is_a?(Token)
27
- new_tag = tag.parse(tag_name, markup, tokens, options)
28
- new_tag.line_number = token.line_number if token.is_a?(Token)
29
- @blank &&= new_tag.blank?
30
- @nodelist << new_tag
31
- else
32
- # end parsing if we reach an unknown tag and let the caller decide
33
- # determine how to proceed
34
- return yield tag_name, markup
35
- end
36
- else
37
- raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
38
- end
39
- when token.start_with?(VARSTART)
40
- new_var = create_variable(token, options)
41
- new_var.line_number = token.line_number if token.is_a?(Token)
42
- @nodelist << new_var
43
- @blank = false
44
- else
45
- @nodelist << token
46
- @blank &&= !!(token =~ /\A\s*\z/)
47
- end
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)
48
25
  end
49
- rescue SyntaxError => e
50
- e.set_line_number_from_token(token)
51
- raise
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)
52
48
  end
49
+ parse_context.line_number = tokenizer.line_number
53
50
  end
54
51
 
55
52
  yield nil, nil
56
53
  end
57
54
 
58
- def blank?
59
- @blank
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)
60
63
  end
61
64
 
62
- def warnings
63
- all_warnings = []
64
- nodelist.each do |node|
65
- all_warnings.concat(node.warnings) if node.respond_to?(:warnings) && node.warnings
66
- end
67
- all_warnings
65
+ def blank?
66
+ @blank
68
67
  end
69
68
 
70
69
  def render(context)
71
70
  output = []
72
- context.resource_limits[:render_length_current] = 0
73
- context.resource_limits[:render_score_current] += @nodelist.length
71
+ context.resource_limits.render_score += @nodelist.length
74
72
 
75
- @nodelist.each do |token|
76
- # Break out if we have any unhanded interrupts.
77
- break if context.has_interrupt?
78
-
79
- begin
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
80
85
  # If we get an Interrupt that means the block must stop processing. An
81
86
  # Interrupt is any command that stops block execution such as {% break %}
82
87
  # or {% continue %}
83
- if token.is_a?(Continue) or token.is_a?(Break)
84
- context.push_interrupt(token.interrupt)
85
- break
86
- end
87
-
88
- token_output = render_token(token, context)
89
-
90
- unless token.is_a?(Block) && token.blank?
91
- output << token_output
92
- end
93
- rescue MemoryError => e
94
- raise e
95
- rescue ::StandardError => e
96
- output << context.handle_error(e, token)
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
97
93
  end
94
+ idx += 1
98
95
  end
99
96
 
100
97
  output.join
@@ -102,22 +99,45 @@ module Liquid
102
99
 
103
100
  private
104
101
 
105
- def render_token(token, context)
106
- token_output = (token.respond_to?(:render) ? token.render(context) : token)
107
- context.increment_used_resources(:render_length_current, token_output)
108
- if context.resource_limits_reached?
109
- context.resource_limits[:reached] = true
110
- raise MemoryError.new("Memory limits exceeded".freeze)
111
- end
112
- token_output
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)
113
115
  end
114
116
 
115
- def create_variable(token, options)
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)
116
124
  token.scan(ContentOfVariable) do |content|
117
- markup = token.is_a?(Token) ? token.child(content.first) : content.first
118
- return Variable.new(markup, options)
125
+ markup = content.first
126
+ return Variable.new(markup, parse_context)
119
127
  end
120
- raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
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
121
141
  end
122
142
  end
123
143
  end
@@ -3,28 +3,33 @@ 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
- '=='.freeze => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
12
- '!='.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
13
- '<>'.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
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
14
  '<'.freeze => :<,
15
15
  '>'.freeze => :>,
16
16
  '>='.freeze => :>=,
17
17
  '<='.freeze => :<=,
18
- 'contains'.freeze => lambda { |cond, left, right|
19
- left && right && left.respond_to?(:include?) ? left.include?(right) : false
20
- }
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
21
26
  }
22
27
 
23
28
  def self.operators
24
29
  @@operators
25
30
  end
26
31
 
27
- attr_reader :attachment
32
+ attr_reader :attachment, :child_condition
28
33
  attr_accessor :left, :operator, :right
29
34
 
30
35
  def initialize(left = nil, operator = nil, right = nil)
@@ -36,16 +41,22 @@ module Liquid
36
41
  end
37
42
 
38
43
  def evaluate(context = Context.new)
39
- result = interpret_condition(left, right, operator, context)
40
-
41
- case @child_relation
42
- when :or
43
- result || @child_condition.evaluate(context)
44
- when :and
45
- result && @child_condition.evaluate(context)
46
- else
47
- 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
48
58
  end
59
+ result
49
60
  end
50
61
 
51
62
  def or(condition)
@@ -70,20 +81,24 @@ module Liquid
70
81
  "#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
71
82
  end
72
83
 
84
+ protected
85
+
86
+ attr_reader :child_relation
87
+
73
88
  private
74
89
 
75
90
  def equal_variables(left, right)
76
- if left.is_a?(Symbol)
77
- if right.respond_to?(left)
78
- 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)
79
94
  else
80
95
  return nil
81
96
  end
82
97
  end
83
98
 
84
- if right.is_a?(Symbol)
85
- if left.respond_to?(right)
86
- 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)
87
102
  else
88
103
  return nil
89
104
  end
@@ -96,36 +111,41 @@ module Liquid
96
111
  # If the operator is empty this means that the decision statement is just
97
112
  # a single variable. We can just poll this variable from the context and
98
113
  # return this as the result.
99
- return context[left] if op == nil
114
+ return context.evaluate(left) if op.nil?
100
115
 
101
- left = context[left]
102
- right = context[right]
116
+ left = context.evaluate(left)
117
+ right = context.evaluate(right)
103
118
 
104
119
  operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
105
120
 
106
121
  if operation.respond_to?(:call)
107
122
  operation.call(self, left, right)
108
- elsif left.respond_to?(operation) and right.respond_to?(operation)
123
+ elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
109
124
  begin
110
125
  left.send(operation, right)
111
126
  rescue ::ArgumentError => e
112
127
  raise Liquid::ArgumentError.new(e.message)
113
128
  end
114
- else
115
- nil
116
129
  end
117
130
  end
118
- end
119
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
120
141
 
121
142
  class ElseCondition < Condition
122
143
  def else?
123
144
  true
124
145
  end
125
146
 
126
- def evaluate(context)
147
+ def evaluate(_context)
127
148
  true
128
149
  end
129
150
  end
130
-
131
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,41 +13,32 @@ module Liquid
14
13
  # context['bob'] #=> nil class Context
15
14
  class Context
16
15
  attr_reader :scopes, :errors, :registers, :environments, :resource_limits
17
- attr_accessor :exception_handler
16
+ attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
18
17
 
19
18
  def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
20
19
  @environments = [environments].flatten
21
20
  @scopes = [(outer_scope || {})]
22
21
  @registers = registers
23
22
  @errors = []
24
- @resource_limits = resource_limits || Template.default_resource_limits.dup
25
- @resource_limits[:render_score_current] = 0
26
- @resource_limits[:assign_score_current] = 0
27
- @parsed_expression = Hash.new{ |cache, markup| cache[markup] = Expression.parse(markup) }
23
+ @partial = false
24
+ @strict_variables = false
25
+ @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
28
26
  squash_instance_assigns_with_environments
29
27
 
30
28
  @this_stack_used = false
31
29
 
30
+ self.exception_renderer = Template.default_exception_renderer
32
31
  if rethrow_errors
33
- self.exception_handler = ->(e) { true }
32
+ self.exception_renderer = ->(e) { raise }
34
33
  end
35
34
 
36
35
  @interrupts = []
37
36
  @filters = []
37
+ @global_filter = nil
38
38
  end
39
39
 
40
- def increment_used_resources(key, obj)
41
- @resource_limits[key] += if obj.kind_of?(String) || obj.kind_of?(Array) || obj.kind_of?(Hash)
42
- obj.length
43
- else
44
- 1
45
- end
46
- end
47
-
48
- def resource_limits_reached?
49
- (@resource_limits[:render_length_limit] && @resource_limits[:render_length_current] > @resource_limits[:render_length_limit]) ||
50
- (@resource_limits[:render_score_limit] && @resource_limits[:render_score_current] > @resource_limits[:render_score_limit] ) ||
51
- (@resource_limits[:assign_score_limit] && @resource_limits[:assign_score_current] > @resource_limits[:assign_score_limit] )
40
+ def warnings
41
+ @warnings ||= []
52
42
  end
53
43
 
54
44
  def strainer
@@ -65,8 +55,12 @@ module Liquid
65
55
  @strainer = nil
66
56
  end
67
57
 
58
+ def apply_global_filter(obj)
59
+ global_filter.nil? ? obj : global_filter.call(obj)
60
+ end
61
+
68
62
  # are there any not handled interrupts?
69
- def has_interrupt?
63
+ def interrupt?
70
64
  !@interrupts.empty?
71
65
  end
72
66
 
@@ -80,15 +74,12 @@ module Liquid
80
74
  @interrupts.pop
81
75
  end
82
76
 
83
-
84
- def handle_error(e, token=nil)
85
- if e.is_a?(Liquid::Error)
86
- e.set_line_number_from_token(token)
87
- end
88
-
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
89
81
  errors.push(e)
90
- raise if exception_handler && exception_handler.call(e)
91
- Liquid::Error.render(e)
82
+ exception_renderer.call(e).to_s
92
83
  end
93
84
 
94
85
  def invoke(method, *args)
@@ -96,9 +87,9 @@ module Liquid
96
87
  end
97
88
 
98
89
  # Push new local scope on the stack. use <tt>Context#stack</tt> instead
99
- def push(new_scope={})
90
+ def push(new_scope = {})
100
91
  @scopes.unshift(new_scope)
101
- raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
92
+ raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH
102
93
  end
103
94
 
104
95
  # Merge a hash of variables in the current local scope
@@ -120,7 +111,7 @@ module Liquid
120
111
  # end
121
112
  #
122
113
  # context['var] #=> nil
123
- def stack(new_scope=nil)
114
+ def stack(new_scope = nil)
124
115
  old_stack_used = @this_stack_used
125
116
  if new_scope
126
117
  push(new_scope)
@@ -157,10 +148,10 @@ module Liquid
157
148
  # Example:
158
149
  # products == empty #=> products.empty?
159
150
  def [](expression)
160
- evaluate(@parsed_expression[expression])
151
+ evaluate(Expression.parse(expression))
161
152
  end
162
153
 
163
- def has_key?(key)
154
+ def key?(key)
164
155
  self[key] != nil
165
156
  end
166
157
 
@@ -169,36 +160,43 @@ module Liquid
169
160
  end
170
161
 
171
162
  # Fetches an object starting at the local scope and then moving up the hierachy
172
- def find_variable(key)
173
-
163
+ def find_variable(key, raise_on_not_found: true)
174
164
  # This was changed from find() to find_index() because this is a very hot
175
165
  # path and find_index() is optimized in MRI to reduce object allocation
176
- index = @scopes.find_index { |s| s.has_key?(key) }
166
+ index = @scopes.find_index { |s| s.key?(key) }
177
167
  scope = @scopes[index] if index
178
168
 
179
169
  variable = nil
180
170
 
181
171
  if scope.nil?
182
172
  @environments.each do |e|
183
- variable = lookup_and_evaluate(e, key)
184
- unless variable.nil?
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
185
177
  scope = e
186
178
  break
187
179
  end
188
180
  end
189
181
  end
190
182
 
191
- scope ||= @environments.last || @scopes.last
192
- variable ||= lookup_and_evaluate(scope, key)
183
+ scope ||= @environments.last || @scopes.last
184
+ variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
193
185
 
194
186
  variable = variable.to_liquid
195
187
  variable.context = self if variable.respond_to?(:context=)
196
188
 
197
- return variable
189
+ variable
198
190
  end
199
191
 
200
- def lookup_and_evaluate(obj, key)
201
- if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
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
196
+
197
+ value = obj[key]
198
+
199
+ if value.is_a?(Proc) && obj.respond_to?(:[]=)
202
200
  obj[key] = (value.arity == 0) ? value.call : value.call(self)
203
201
  else
204
202
  value
@@ -206,15 +204,23 @@ module Liquid
206
204
  end
207
205
 
208
206
  private
209
- def squash_instance_assigns_with_environments
210
- @scopes.last.each_key do |k|
211
- @environments.each do |env|
212
- if env.has_key?(k)
213
- scopes.last[k] = lookup_and_evaluate(env, k)
214
- break
215
- end
207
+
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
214
+
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
216
221
  end
217
222
  end
218
- end # squash_instance_assigns_with_environments
223
+ end
224
+ end # squash_instance_assigns_with_environments
219
225
  end # Context
220
226
  end # Liquid
@@ -1,17 +1,27 @@
1
1
  module Liquid
2
- class Document < Block
3
- def self.parse(tokens, options={})
4
- # we don't need markup to open this block
5
- super(nil, nil, tokens, options)
2
+ class Document < BlockBody
3
+ def self.parse(tokens, parse_context)
4
+ doc = new
5
+ doc.parse(tokens, parse_context)
6
+ doc
6
7
  end
7
8
 
8
- # There isn't a real delimiter
9
- def block_delimiter
10
- []
9
+ def parse(tokens, parse_context)
10
+ super do |end_tag_name, end_tag_params|
11
+ unknown_tag(end_tag_name, parse_context) if end_tag_name
12
+ end
13
+ rescue SyntaxError => e
14
+ e.line_number ||= parse_context.line_number
15
+ raise
11
16
  end
12
17
 
13
- # Document blocks don't need to be terminated since they are not actually opened
14
- def assert_missing_delimitation!
18
+ def unknown_tag(tag, parse_context)
19
+ case tag
20
+ when 'else'.freeze, 'end'.freeze
21
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
22
+ else
23
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
24
+ end
15
25
  end
16
26
  end
17
27
  end