liquid 3.0.0 → 4.0.0

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 (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