liquid 3.0.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +130 -62
  3. data/README.md +31 -0
  4. data/lib/liquid/block.rb +31 -124
  5. data/lib/liquid/block_body.rb +75 -59
  6. data/lib/liquid/condition.rb +23 -22
  7. data/lib/liquid/context.rb +51 -60
  8. data/lib/liquid/document.rb +19 -9
  9. data/lib/liquid/drop.rb +17 -16
  10. data/lib/liquid/errors.rb +20 -24
  11. data/lib/liquid/expression.rb +15 -3
  12. data/lib/liquid/extensions.rb +13 -7
  13. data/lib/liquid/file_system.rb +11 -11
  14. data/lib/liquid/forloop_drop.rb +42 -0
  15. data/lib/liquid/i18n.rb +5 -5
  16. data/lib/liquid/interrupts.rb +1 -2
  17. data/lib/liquid/lexer.rb +6 -4
  18. data/lib/liquid/locales/en.yml +5 -1
  19. data/lib/liquid/parse_context.rb +37 -0
  20. data/lib/liquid/parser.rb +1 -1
  21. data/lib/liquid/parser_switching.rb +4 -4
  22. data/lib/liquid/profiler/hooks.rb +7 -7
  23. data/lib/liquid/profiler.rb +18 -19
  24. data/lib/liquid/range_lookup.rb +16 -1
  25. data/lib/liquid/resource_limits.rb +23 -0
  26. data/lib/liquid/standardfilters.rb +121 -61
  27. data/lib/liquid/strainer.rb +32 -29
  28. data/lib/liquid/tablerowloop_drop.rb +62 -0
  29. data/lib/liquid/tag.rb +9 -8
  30. data/lib/liquid/tags/assign.rb +17 -4
  31. data/lib/liquid/tags/break.rb +0 -3
  32. data/lib/liquid/tags/capture.rb +2 -2
  33. data/lib/liquid/tags/case.rb +19 -12
  34. data/lib/liquid/tags/comment.rb +2 -2
  35. data/lib/liquid/tags/cycle.rb +6 -6
  36. data/lib/liquid/tags/decrement.rb +1 -4
  37. data/lib/liquid/tags/for.rb +95 -75
  38. data/lib/liquid/tags/if.rb +48 -43
  39. data/lib/liquid/tags/ifchanged.rb +0 -2
  40. data/lib/liquid/tags/include.rb +61 -52
  41. data/lib/liquid/tags/raw.rb +32 -4
  42. data/lib/liquid/tags/table_row.rb +12 -31
  43. data/lib/liquid/tags/unless.rb +4 -5
  44. data/lib/liquid/template.rb +42 -54
  45. data/lib/liquid/tokenizer.rb +31 -0
  46. data/lib/liquid/utils.rb +52 -8
  47. data/lib/liquid/variable.rb +46 -45
  48. data/lib/liquid/variable_lookup.rb +9 -5
  49. data/lib/liquid/version.rb +1 -1
  50. data/lib/liquid.rb +9 -7
  51. data/test/integration/assign_test.rb +18 -8
  52. data/test/integration/blank_test.rb +14 -14
  53. data/test/integration/capture_test.rb +10 -0
  54. data/test/integration/context_test.rb +2 -2
  55. data/test/integration/document_test.rb +19 -0
  56. data/test/integration/drop_test.rb +42 -40
  57. data/test/integration/error_handling_test.rb +99 -46
  58. data/test/integration/filter_test.rb +72 -19
  59. data/test/integration/hash_ordering_test.rb +9 -9
  60. data/test/integration/output_test.rb +34 -27
  61. data/test/integration/parsing_quirks_test.rb +15 -13
  62. data/test/integration/render_profiling_test.rb +20 -20
  63. data/test/integration/security_test.rb +9 -7
  64. data/test/integration/standard_filter_test.rb +198 -42
  65. data/test/integration/tags/break_tag_test.rb +1 -2
  66. data/test/integration/tags/continue_tag_test.rb +0 -1
  67. data/test/integration/tags/for_tag_test.rb +133 -98
  68. data/test/integration/tags/if_else_tag_test.rb +96 -77
  69. data/test/integration/tags/include_tag_test.rb +34 -30
  70. data/test/integration/tags/increment_tag_test.rb +10 -11
  71. data/test/integration/tags/raw_tag_test.rb +7 -1
  72. data/test/integration/tags/standard_tag_test.rb +121 -122
  73. data/test/integration/tags/statements_test.rb +3 -5
  74. data/test/integration/tags/table_row_test.rb +20 -19
  75. data/test/integration/tags/unless_else_tag_test.rb +6 -6
  76. data/test/integration/template_test.rb +190 -49
  77. data/test/integration/trim_mode_test.rb +525 -0
  78. data/test/integration/variable_test.rb +23 -13
  79. data/test/test_helper.rb +44 -9
  80. data/test/unit/block_unit_test.rb +8 -5
  81. data/test/unit/condition_unit_test.rb +86 -77
  82. data/test/unit/context_unit_test.rb +48 -57
  83. data/test/unit/file_system_unit_test.rb +3 -3
  84. data/test/unit/i18n_unit_test.rb +2 -2
  85. data/test/unit/lexer_unit_test.rb +11 -8
  86. data/test/unit/parser_unit_test.rb +2 -2
  87. data/test/unit/regexp_unit_test.rb +1 -1
  88. data/test/unit/strainer_unit_test.rb +85 -8
  89. data/test/unit/tag_unit_test.rb +7 -2
  90. data/test/unit/tags/case_tag_unit_test.rb +1 -1
  91. data/test/unit/tags/for_tag_unit_test.rb +2 -2
  92. data/test/unit/tags/if_tag_unit_test.rb +1 -1
  93. data/test/unit/template_unit_test.rb +14 -5
  94. data/test/unit/tokenizer_unit_test.rb +24 -7
  95. data/test/unit/variable_unit_test.rb +66 -43
  96. metadata +55 -50
  97. data/lib/liquid/module_ex.rb +0 -62
  98. data/lib/liquid/token.rb +0 -18
  99. data/test/unit/module_ex_unit_test.rb +0 -87
  100. /data/{MIT-LICENSE → LICENSE} +0 -0
@@ -1,7 +1,7 @@
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
5
  TAGSTART = "{%".freeze
6
6
  VARSTART = "{{".freeze
7
7
 
@@ -12,88 +12,91 @@ module Liquid
12
12
  @blank = true
13
13
  end
14
14
 
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
15
+ def parse(tokenizer, parse_context)
16
+ parse_context.line_number = tokenizer.line_number
17
+ while token = tokenizer.shift
18
+ unless token.empty?
19
+ case
20
+ when token.start_with?(TAGSTART)
21
+ whitespace_handler(token, parse_context)
22
+ if token =~ FullToken
23
+ tag_name = $1
24
+ markup = $2
25
+ # fetch the tag from registered blocks
26
+ if tag = registered_tags[tag_name]
27
+ new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
28
+ @blank &&= new_tag.blank?
29
+ @nodelist << new_tag
36
30
  else
37
- raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
31
+ # end parsing if we reach an unknown tag and let the caller decide
32
+ # determine how to proceed
33
+ return yield tag_name, markup
38
34
  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
35
  else
45
- @nodelist << token
46
- @blank &&= !!(token =~ /\A\s*\z/)
36
+ raise_missing_tag_terminator(token, parse_context)
37
+ end
38
+ when token.start_with?(VARSTART)
39
+ whitespace_handler(token, parse_context)
40
+ @nodelist << create_variable(token, parse_context)
41
+ @blank = false
42
+ else
43
+ if parse_context.trim_whitespace
44
+ token.lstrip!
47
45
  end
46
+ parse_context.trim_whitespace = false
47
+ @nodelist << token
48
+ @blank &&= !!(token =~ /\A\s*\z/)
48
49
  end
49
- rescue SyntaxError => e
50
- e.set_line_number_from_token(token)
51
- raise
52
50
  end
51
+ parse_context.line_number = tokenizer.line_number
53
52
  end
54
53
 
55
54
  yield nil, nil
56
55
  end
57
56
 
58
- def blank?
59
- @blank
57
+ def whitespace_handler(token, parse_context)
58
+ if token[2] == WhitespaceControl
59
+ previous_token = @nodelist.last
60
+ if previous_token.is_a? String
61
+ previous_token.rstrip!
62
+ end
63
+ end
64
+ parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
60
65
  end
61
66
 
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
67
+ def blank?
68
+ @blank
68
69
  end
69
70
 
70
71
  def render(context)
71
72
  output = []
72
- context.resource_limits[:render_length_current] = 0
73
- context.resource_limits[:render_score_current] += @nodelist.length
73
+ context.resource_limits.render_score += @nodelist.length
74
74
 
75
75
  @nodelist.each do |token|
76
76
  # Break out if we have any unhanded interrupts.
77
- break if context.has_interrupt?
77
+ break if context.interrupt?
78
78
 
79
79
  begin
80
80
  # If we get an Interrupt that means the block must stop processing. An
81
81
  # Interrupt is any command that stops block execution such as {% break %}
82
82
  # or {% continue %}
83
- if token.is_a?(Continue) or token.is_a?(Break)
83
+ if token.is_a?(Continue) || token.is_a?(Break)
84
84
  context.push_interrupt(token.interrupt)
85
85
  break
86
86
  end
87
87
 
88
- token_output = render_token(token, context)
88
+ node_output = render_node(token, context)
89
89
 
90
90
  unless token.is_a?(Block) && token.blank?
91
- output << token_output
91
+ output << node_output
92
92
  end
93
93
  rescue MemoryError => e
94
94
  raise e
95
+ rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
96
+ context.handle_error(e, token.line_number, token.raw)
97
+ output << nil
95
98
  rescue ::StandardError => e
96
- output << context.handle_error(e, token)
99
+ output << context.handle_error(e, token.line_number, token.raw)
97
100
  end
98
101
  end
99
102
 
@@ -102,22 +105,35 @@ module Liquid
102
105
 
103
106
  private
104
107
 
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
108
+ def render_node(node, context)
109
+ node_output = (node.respond_to?(:render) ? node.render(context) : node)
110
+ node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
111
+
112
+ context.resource_limits.render_length += node_output.length
113
+ if context.resource_limits.reached?
110
114
  raise MemoryError.new("Memory limits exceeded".freeze)
111
115
  end
112
- token_output
116
+ node_output
113
117
  end
114
118
 
115
- def create_variable(token, options)
119
+ def create_variable(token, parse_context)
116
120
  token.scan(ContentOfVariable) do |content|
117
- markup = token.is_a?(Token) ? token.child(content.first) : content.first
118
- return Variable.new(markup, options)
121
+ markup = content.first
122
+ return Variable.new(markup, parse_context)
119
123
  end
120
- raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
124
+ raise_missing_variable_terminator(token, parse_context)
125
+ end
126
+
127
+ def raise_missing_tag_terminator(token, parse_context)
128
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
129
+ end
130
+
131
+ def raise_missing_variable_terminator(token, parse_context)
132
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
133
+ end
134
+
135
+ def registered_tags
136
+ Template.tags
121
137
  end
122
138
  end
123
139
  end
@@ -3,21 +3,26 @@ 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
@@ -73,17 +78,17 @@ module Liquid
73
78
  private
74
79
 
75
80
  def equal_variables(left, right)
76
- if left.is_a?(Symbol)
77
- if right.respond_to?(left)
78
- return right.send(left.to_s)
81
+ if left.is_a?(Liquid::Expression::MethodLiteral)
82
+ if right.respond_to?(left.method_name)
83
+ return right.send(left.method_name)
79
84
  else
80
85
  return nil
81
86
  end
82
87
  end
83
88
 
84
- if right.is_a?(Symbol)
85
- if left.respond_to?(right)
86
- return left.send(right.to_s)
89
+ if right.is_a?(Liquid::Expression::MethodLiteral)
90
+ if left.respond_to?(right.method_name)
91
+ return left.send(right.method_name)
87
92
  else
88
93
  return nil
89
94
  end
@@ -96,36 +101,32 @@ module Liquid
96
101
  # If the operator is empty this means that the decision statement is just
97
102
  # a single variable. We can just poll this variable from the context and
98
103
  # return this as the result.
99
- return context[left] if op == nil
104
+ return context.evaluate(left) if op.nil?
100
105
 
101
- left = context[left]
102
- right = context[right]
106
+ left = context.evaluate(left)
107
+ right = context.evaluate(right)
103
108
 
104
109
  operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
105
110
 
106
111
  if operation.respond_to?(:call)
107
112
  operation.call(self, left, right)
108
- elsif left.respond_to?(operation) and right.respond_to?(operation)
113
+ elsif left.respond_to?(operation) && right.respond_to?(operation)
109
114
  begin
110
115
  left.send(operation, right)
111
116
  rescue ::ArgumentError => e
112
117
  raise Liquid::ArgumentError.new(e.message)
113
118
  end
114
- else
115
- nil
116
119
  end
117
120
  end
118
121
  end
119
122
 
120
-
121
123
  class ElseCondition < Condition
122
124
  def else?
123
125
  true
124
126
  end
125
127
 
126
- def evaluate(context)
128
+ def evaluate(_context)
127
129
  true
128
130
  end
129
131
  end
130
-
131
132
  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
@@ -61,25 +51,16 @@ module Liquid
61
51
  # for that
62
52
  def add_filters(filters)
63
53
  filters = [filters].flatten.compact
64
- filters.each do |f|
65
- raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
66
- Strainer.add_known_filter(f)
67
- end
54
+ @filters += filters
55
+ @strainer = nil
56
+ end
68
57
 
69
- # If strainer is already setup then there's no choice but to use a runtime
70
- # extend call. If strainer is not yet created, we can utilize strainers
71
- # cached class based API, which avoids busting the method cache.
72
- if @strainer
73
- filters.each do |f|
74
- strainer.extend(f)
75
- end
76
- else
77
- @filters.concat filters
78
- end
58
+ def apply_global_filter(obj)
59
+ global_filter.nil? ? obj : global_filter.call(obj)
79
60
  end
80
61
 
81
62
  # are there any not handled interrupts?
82
- def has_interrupt?
63
+ def interrupt?
83
64
  !@interrupts.empty?
84
65
  end
85
66
 
@@ -93,15 +74,12 @@ module Liquid
93
74
  @interrupts.pop
94
75
  end
95
76
 
96
-
97
- def handle_error(e, token=nil)
98
- if e.is_a?(Liquid::Error)
99
- e.set_line_number_from_token(token)
100
- end
101
-
77
+ def handle_error(e, line_number = nil, raw_token = nil)
78
+ e = internal_error unless e.is_a?(Liquid::Error)
79
+ e.template_name ||= template_name
80
+ e.line_number ||= line_number
102
81
  errors.push(e)
103
- raise if exception_handler && exception_handler.call(e)
104
- Liquid::Error.render(e)
82
+ exception_renderer.call(e).to_s
105
83
  end
106
84
 
107
85
  def invoke(method, *args)
@@ -109,7 +87,7 @@ module Liquid
109
87
  end
110
88
 
111
89
  # Push new local scope on the stack. use <tt>Context#stack</tt> instead
112
- def push(new_scope={})
90
+ def push(new_scope = {})
113
91
  @scopes.unshift(new_scope)
114
92
  raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
115
93
  end
@@ -133,7 +111,7 @@ module Liquid
133
111
  # end
134
112
  #
135
113
  # context['var] #=> nil
136
- def stack(new_scope=nil)
114
+ def stack(new_scope = nil)
137
115
  old_stack_used = @this_stack_used
138
116
  if new_scope
139
117
  push(new_scope)
@@ -170,10 +148,10 @@ module Liquid
170
148
  # Example:
171
149
  # products == empty #=> products.empty?
172
150
  def [](expression)
173
- evaluate(@parsed_expression[expression])
151
+ evaluate(Expression.parse(expression))
174
152
  end
175
153
 
176
- def has_key?(key)
154
+ def key?(key)
177
155
  self[key] != nil
178
156
  end
179
157
 
@@ -183,10 +161,9 @@ module Liquid
183
161
 
184
162
  # Fetches an object starting at the local scope and then moving up the hierachy
185
163
  def find_variable(key)
186
-
187
164
  # This was changed from find() to find_index() because this is a very hot
188
165
  # path and find_index() is optimized in MRI to reduce object allocation
189
- index = @scopes.find_index { |s| s.has_key?(key) }
166
+ index = @scopes.find_index { |s| s.key?(key) }
190
167
  scope = @scopes[index] if index
191
168
 
192
169
  variable = nil
@@ -201,17 +178,23 @@ module Liquid
201
178
  end
202
179
  end
203
180
 
204
- scope ||= @environments.last || @scopes.last
205
- variable ||= lookup_and_evaluate(scope, key)
181
+ scope ||= @environments.last || @scopes.last
182
+ variable ||= lookup_and_evaluate(scope, key)
206
183
 
207
184
  variable = variable.to_liquid
208
185
  variable.context = self if variable.respond_to?(:context=)
209
186
 
210
- return variable
187
+ variable
211
188
  end
212
189
 
213
190
  def lookup_and_evaluate(obj, key)
214
- if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
191
+ if @strict_variables && obj.respond_to?(:key?) && !obj.key?(key)
192
+ raise Liquid::UndefinedVariable, "undefined variable #{key}"
193
+ end
194
+
195
+ value = obj[key]
196
+
197
+ if value.is_a?(Proc) && obj.respond_to?(:[]=)
215
198
  obj[key] = (value.arity == 0) ? value.call : value.call(self)
216
199
  else
217
200
  value
@@ -219,15 +202,23 @@ module Liquid
219
202
  end
220
203
 
221
204
  private
222
- def squash_instance_assigns_with_environments
223
- @scopes.last.each_key do |k|
224
- @environments.each do |env|
225
- if env.has_key?(k)
226
- scopes.last[k] = lookup_and_evaluate(env, k)
227
- break
228
- end
205
+
206
+ def internal_error
207
+ # raise and catch to set backtrace and cause on exception
208
+ raise Liquid::InternalError, 'internal'
209
+ rescue Liquid::InternalError => exc
210
+ exc
211
+ end
212
+
213
+ def squash_instance_assigns_with_environments
214
+ @scopes.last.each_key do |k|
215
+ @environments.each do |env|
216
+ if env.key?(k)
217
+ scopes.last[k] = lookup_and_evaluate(env, k)
218
+ break
229
219
  end
230
220
  end
231
- end # squash_instance_assigns_with_environments
221
+ end
222
+ end # squash_instance_assigns_with_environments
232
223
  end # Context
233
224
  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
data/lib/liquid/drop.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  require 'set'
2
2
 
3
3
  module Liquid
4
-
5
4
  # A drop in liquid is a class which allows you to export DOM like things to liquid.
6
5
  # Methods of drops are callable.
7
6
  # The main use for liquid drops is to implement lazy loaded objects.
@@ -19,28 +18,27 @@ module Liquid
19
18
  # tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
20
19
  # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
21
20
  #
22
- # Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
23
- # catch all.
21
+ # Your drop can either implement the methods sans any parameters
22
+ # or implement the liquid_method_missing(name) method which is a catch all.
24
23
  class Drop
25
24
  attr_writer :context
26
25
 
27
- EMPTY_STRING = ''.freeze
28
-
29
26
  # Catch all for the method
30
- def before_method(method)
31
- nil
27
+ def liquid_method_missing(method)
28
+ return nil unless @context && @context.strict_variables
29
+ raise Liquid::UndefinedDropMethod, "undefined method #{method}"
32
30
  end
33
31
 
34
32
  # called by liquid to invoke a drop
35
33
  def invoke_drop(method_or_key)
36
- if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
34
+ if self.class.invokable?(method_or_key)
37
35
  send(method_or_key)
38
36
  else
39
- before_method(method_or_key)
37
+ liquid_method_missing(method_or_key)
40
38
  end
41
39
  end
42
40
 
43
- def has_key?(name)
41
+ def key?(_name)
44
42
  true
45
43
  end
46
44
 
@@ -56,22 +54,25 @@ module Liquid
56
54
  self.class.name
57
55
  end
58
56
 
59
- alias :[] :invoke_drop
60
-
61
- private
57
+ alias_method :[], :invoke_drop
62
58
 
63
59
  # Check for method existence without invoking respond_to?, which creates symbols
64
60
  def self.invokable?(method_name)
65
- unless @invokable_methods
61
+ invokable_methods.include?(method_name.to_s)
62
+ end
63
+
64
+ def self.invokable_methods
65
+ @invokable_methods ||= begin
66
66
  blacklist = Liquid::Drop.public_instance_methods + [:each]
67
+
67
68
  if include?(Enumerable)
68
69
  blacklist += Enumerable.public_instance_methods
69
70
  blacklist -= [:sort, :count, :first, :min, :max, :include?]
70
71
  end
72
+
71
73
  whitelist = [:to_liquid] + (public_instance_methods - blacklist)
72
- @invokable_methods = Set.new(whitelist.map(&:to_s))
74
+ Set.new(whitelist.map(&:to_s))
73
75
  end
74
- @invokable_methods.include?(method_name.to_s)
75
76
  end
76
77
  end
77
78
  end