liquid 4.0.0 → 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 68c0e92a0c24e19463a4ef68801093b2be0211bb
4
- data.tar.gz: d80ceddfb328477c4fd8ed52faa893088bfd86ef
2
+ SHA256:
3
+ metadata.gz: 55d4aa3aeeef9a99c5601c5c8bbaab62b0c909c9aa3df0ac181e01373a6ed069
4
+ data.tar.gz: 1e3eddbb13e867eca4f90efd32fbbc2609e4235b77d005efaa93711f8e476c40
5
5
  SHA512:
6
- metadata.gz: 2232e4b2053dbcbad922fa89b53c79366aad0ad2f649a9d8e0dec5d6100c572c88bc8ec3a941df8063138cb02ffb9f68269dac3581d780c695b2c96e940cb366
7
- data.tar.gz: 3695492ac392acc500f6ee10758dc5d15991289f0ca5fd16e0c770056cf0a4db928b2d7d29af1f7bb9c1eb4951c7e42854ab48409d373d6780ddd7ce2c27f357
6
+ metadata.gz: 349a44c983e69443a0350d3aa7de2fffed15bf67356a6ce490583413d60dd926cc32907b1fca4d04ca075dd92e17434176f94552237ec5ae549477ccbbff4042
7
+ data.tar.gz: 31d9c4fbe841ad2c3a6549e1e6d02f580b31a538186e83140bd297fb47caf23387795a48468886e82562f661231890cb42637ecbe369dc4583d560055e94ca77
data/History.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Liquid Change Log
2
2
 
3
- ## 4.0.0 / not yet released / branch "master"
3
+ ## 4.0.0 / 2016-12-14 / branch "4-0-stable"
4
4
 
5
5
  ### Changed
6
6
  * Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith]
@@ -20,10 +20,13 @@
20
20
  * Add concat filter to concatenate arrays (#429) [Diogo Beato]
21
21
  * Ruby 1.9 support dropped (#491) [Justin Li]
22
22
  * Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
23
- * Remove support for `liquid_methods`
23
+ * Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement)
24
24
  * Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
25
25
 
26
26
  ### Fixed
27
+
28
+ * Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell]
29
+ * Fix include tag used with strict_variables (#828) [QuickPay]
27
30
  * Fix map filter when value is a Proc (#672) [Guillaume Malette]
28
31
  * Fix truncate filter when value is not a string (#672) [Guillaume Malette]
29
32
  * Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]
data/README.md CHANGED
@@ -42,6 +42,8 @@ Liquid is a template engine which was written with very specific requirements:
42
42
 
43
43
  ## How to use Liquid
44
44
 
45
+ Install Liquid by adding `gem 'liquid'` to your gemfile.
46
+
45
47
  Liquid supports a very simple API based around the Liquid::Template class.
46
48
  For standard use you can just pass it the content of a file and call render with a parameters hash.
47
49
 
@@ -1,5 +1,7 @@
1
1
  module Liquid
2
2
  class Block < Tag
3
+ MAX_DEPTH = 100
4
+
3
5
  def initialize(tag_name, markup, options)
4
6
  super
5
7
  @blank = true
@@ -24,12 +26,12 @@ module Liquid
24
26
  end
25
27
 
26
28
  def unknown_tag(tag, _params, _tokens)
27
- case tag
28
- when 'else'.freeze
29
+ if tag == 'else'.freeze
29
30
  raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
30
31
  block_name: block_name))
31
- when 'end'.freeze
32
+ elsif tag.start_with?('end'.freeze)
32
33
  raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
34
+ tag: tag,
33
35
  block_name: block_name,
34
36
  block_delimiter: block_delimiter))
35
37
  else
@@ -48,17 +50,25 @@ module Liquid
48
50
  protected
49
51
 
50
52
  def parse_body(body, tokens)
51
- body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
52
- @blank &&= body.blank?
53
+ if parse_context.depth >= MAX_DEPTH
54
+ raise StackLevelError, "Nesting too deep".freeze
55
+ end
56
+ parse_context.depth += 1
57
+ begin
58
+ body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
59
+ @blank &&= body.blank?
53
60
 
54
- return false if end_tag_name == block_delimiter
55
- unless end_tag_name
56
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
57
- end
61
+ return false if end_tag_name == block_delimiter
62
+ unless end_tag_name
63
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
64
+ end
58
65
 
59
- # this tag is not registered with the system
60
- # pass it to the current block for special handling or error reporting
61
- unknown_tag(end_tag_name, end_tag_params, tokens)
66
+ # this tag is not registered with the system
67
+ # pass it to the current block for special handling or error reporting
68
+ unknown_tag(end_tag_name, end_tag_params, tokens)
69
+ end
70
+ ensure
71
+ parse_context.depth -= 1
62
72
  end
63
73
 
64
74
  true
@@ -2,6 +2,7 @@ module Liquid
2
2
  class BlockBody
3
3
  FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
4
4
  ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
5
+ WhitespaceOrNothing = /\A\s*\z/
5
6
  TAGSTART = "{%".freeze
6
7
  VARSTART = "{{".freeze
7
8
 
@@ -15,38 +16,35 @@ module Liquid
15
16
  def parse(tokenizer, parse_context)
16
17
  parse_context.line_number = tokenizer.line_number
17
18
  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
30
- else
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
34
- end
35
- else
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!
45
- end
46
- parse_context.trim_whitespace = false
47
- @nodelist << token
48
- @blank &&= !!(token =~ /\A\s*\z/)
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)
49
25
  end
26
+ tag_name = $1
27
+ markup = $2
28
+ # fetch the tag from registered blocks
29
+ unless tag = registered_tags[tag_name]
30
+ # end parsing if we reach an unknown tag and let the caller decide
31
+ # determine how to proceed
32
+ return yield tag_name, markup
33
+ end
34
+ new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
35
+ @blank &&= new_tag.blank?
36
+ @nodelist << new_tag
37
+ when token.start_with?(VARSTART)
38
+ whitespace_handler(token, parse_context)
39
+ @nodelist << create_variable(token, parse_context)
40
+ @blank = false
41
+ else
42
+ if parse_context.trim_whitespace
43
+ token.lstrip!
44
+ end
45
+ parse_context.trim_whitespace = false
46
+ @nodelist << token
47
+ @blank &&= !!(token =~ WhitespaceOrNothing)
50
48
  end
51
49
  parse_context.line_number = tokenizer.line_number
52
50
  end
@@ -72,32 +70,27 @@ module Liquid
72
70
  output = []
73
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.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) || token.is_a?(Break)
84
- context.push_interrupt(token.interrupt)
85
- break
86
- end
87
-
88
- node_output = render_node(token, context)
89
-
90
- unless token.is_a?(Block) && token.blank?
91
- output << node_output
92
- end
93
- rescue MemoryError => e
94
- raise e
95
- rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
96
- context.handle_error(e, token.line_number, token.raw)
97
- output << nil
98
- rescue ::StandardError => e
99
- output << context.handle_error(e, token.line_number, token.raw)
88
+ context.push_interrupt(node.interrupt)
89
+ break
90
+ else # Other non-Block tags
91
+ render_node_to_output(node, output, context)
100
92
  end
93
+ idx += 1
101
94
  end
102
95
 
103
96
  output.join
@@ -105,15 +98,25 @@ module Liquid
105
98
 
106
99
  private
107
100
 
108
- def render_node(node, context)
109
- node_output = (node.respond_to?(:render) ? node.render(context) : node)
101
+ def render_node_to_output(node, output, context, skip_output = false)
102
+ node_output = node.render(context)
110
103
  node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
104
+ check_resources(context, node_output)
105
+ output << node_output unless skip_output
106
+ rescue MemoryError => e
107
+ raise e
108
+ rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
109
+ context.handle_error(e, node.line_number)
110
+ output << nil
111
+ rescue ::StandardError => e
112
+ line_number = node.is_a?(String) ? nil : node.line_number
113
+ output << context.handle_error(e, line_number)
114
+ end
111
115
 
116
+ def check_resources(context, node_output)
112
117
  context.resource_limits.render_length += node_output.length
113
- if context.resource_limits.reached?
114
- raise MemoryError.new("Memory limits exceeded".freeze)
115
- end
116
- node_output
118
+ return unless context.resource_limits.reached?
119
+ raise MemoryError.new("Memory limits exceeded".freeze)
117
120
  end
118
121
 
119
122
  def create_variable(token, parse_context)
@@ -41,16 +41,22 @@ module Liquid
41
41
  end
42
42
 
43
43
  def evaluate(context = Context.new)
44
- result = interpret_condition(left, right, operator, context)
45
-
46
- case @child_relation
47
- when :or
48
- result || @child_condition.evaluate(context)
49
- when :and
50
- result && @child_condition.evaluate(context)
51
- else
52
- 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
53
58
  end
59
+ result
54
60
  end
55
61
 
56
62
  def or(condition)
@@ -75,6 +81,10 @@ module Liquid
75
81
  "#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
76
82
  end
77
83
 
84
+ protected
85
+
86
+ attr_reader :child_relation, :child_condition
87
+
78
88
  private
79
89
 
80
90
  def equal_variables(left, right)
@@ -110,7 +120,7 @@ module Liquid
110
120
 
111
121
  if operation.respond_to?(:call)
112
122
  operation.call(self, left, right)
113
- elsif left.respond_to?(operation) && right.respond_to?(operation)
123
+ elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
114
124
  begin
115
125
  left.send(operation, right)
116
126
  rescue ::ArgumentError => e
@@ -74,7 +74,7 @@ module Liquid
74
74
  @interrupts.pop
75
75
  end
76
76
 
77
- def handle_error(e, line_number = nil, raw_token = nil)
77
+ def handle_error(e, line_number = nil)
78
78
  e = internal_error unless e.is_a?(Liquid::Error)
79
79
  e.template_name ||= template_name
80
80
  e.line_number ||= line_number
@@ -89,7 +89,7 @@ module Liquid
89
89
  # Push new local scope on the stack. use <tt>Context#stack</tt> instead
90
90
  def push(new_scope = {})
91
91
  @scopes.unshift(new_scope)
92
- raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
92
+ raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH
93
93
  end
94
94
 
95
95
  # Merge a hash of variables in the current local scope
@@ -160,7 +160,7 @@ module Liquid
160
160
  end
161
161
 
162
162
  # Fetches an object starting at the local scope and then moving up the hierachy
163
- def find_variable(key)
163
+ def find_variable(key, raise_on_not_found: true)
164
164
  # This was changed from find() to find_index() because this is a very hot
165
165
  # path and find_index() is optimized in MRI to reduce object allocation
166
166
  index = @scopes.find_index { |s| s.key?(key) }
@@ -170,8 +170,10 @@ module Liquid
170
170
 
171
171
  if scope.nil?
172
172
  @environments.each do |e|
173
- variable = lookup_and_evaluate(e, key)
174
- 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
175
177
  scope = e
176
178
  break
177
179
  end
@@ -179,7 +181,7 @@ module Liquid
179
181
  end
180
182
 
181
183
  scope ||= @environments.last || @scopes.last
182
- variable ||= lookup_and_evaluate(scope, key)
184
+ variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
183
185
 
184
186
  variable = variable.to_liquid
185
187
  variable.context = self if variable.respond_to?(:context=)
@@ -187,8 +189,8 @@ module Liquid
187
189
  variable
188
190
  end
189
191
 
190
- def lookup_and_evaluate(obj, key)
191
- if @strict_variables && obj.respond_to?(:key?) && !obj.key?(key)
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)
192
194
  raise Liquid::UndefinedVariable, "undefined variable #{key}"
193
195
  end
194
196
 
@@ -21,20 +21,24 @@ module Liquid
21
21
  'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
22
22
  }
23
23
 
24
+ SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
25
+ DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
26
+ INTEGERS_REGEX = /\A(-?\d+)\z/
27
+ FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
28
+ RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
29
+
24
30
  def self.parse(markup)
25
31
  if LITERALS.key?(markup)
26
32
  LITERALS[markup]
27
33
  else
28
34
  case markup
29
- when /\A'(.*)'\z/m # Single quoted strings
30
- $1
31
- when /\A"(.*)"\z/m # Double quoted strings
35
+ when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
32
36
  $1
33
- when /\A(-?\d+)\z/ # Integer and floats
37
+ when INTEGERS_REGEX
34
38
  $1.to_i
35
- when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
39
+ when RANGES_REGEX
36
40
  RangeLookup.parse($1, $2)
37
- when /\A(-?\d[\d\.]+)\z/ # Floats
41
+ when FLOATS_REGEX
38
42
  $1.to_f
39
43
  else
40
44
  VariableLookup.parse(markup)
@@ -7,6 +7,12 @@ class String # :nodoc:
7
7
  end
8
8
  end
9
9
 
10
+ class Symbol # :nodoc:
11
+ def to_liquid
12
+ to_s
13
+ end
14
+ end
15
+
10
16
  class Array # :nodoc:
11
17
  def to_liquid
12
18
  self
@@ -18,17 +18,19 @@ module Liquid
18
18
  DOUBLE_STRING_LITERAL = /"[^\"]*"/
19
19
  NUMBER_LITERAL = /-?\d+(\.\d+)?/
20
20
  DOTDOT = /\.\./
21
- COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
21
+ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
22
+ WHITESPACE_OR_NOTHING = /\s*/
22
23
 
23
24
  def initialize(input)
24
- @ss = StringScanner.new(input.rstrip)
25
+ @ss = StringScanner.new(input)
25
26
  end
26
27
 
27
28
  def tokenize
28
29
  @output = []
29
30
 
30
31
  until @ss.eos?
31
- @ss.skip(/\s*/)
32
+ @ss.skip(WHITESPACE_OR_NOTHING)
33
+ break if @ss.eos?
32
34
  tok = case
33
35
  when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
34
36
  when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
@@ -14,7 +14,7 @@
14
14
  if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
15
15
  include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
16
16
  unknown_tag: "Unknown tag '%{tag}'"
17
- invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
17
+ invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
18
18
  unexpected_else: "%{block_name} tag does not expect 'else' tag"
19
19
  unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
20
20
  tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"