liquid 3.0.6 → 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 (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