liquid 4.0.0 → 4.0.1

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.
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}"