liquid 4.0.1 → 4.0.2

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
2
  SHA256:
3
- metadata.gz: 55d4aa3aeeef9a99c5601c5c8bbaab62b0c909c9aa3df0ac181e01373a6ed069
4
- data.tar.gz: 1e3eddbb13e867eca4f90efd32fbbc2609e4235b77d005efaa93711f8e476c40
3
+ metadata.gz: e3124f91e43cab43cf6782b093c0387d9189bd951b911ab1ade84024e5b84bf3
4
+ data.tar.gz: 40c04d46b3a1a51a775832cabf161fc870bde43b72d2857a8a8cf736e1909a43
5
5
  SHA512:
6
- metadata.gz: 349a44c983e69443a0350d3aa7de2fffed15bf67356a6ce490583413d60dd926cc32907b1fca4d04ca075dd92e17434176f94552237ec5ae549477ccbbff4042
7
- data.tar.gz: 31d9c4fbe841ad2c3a6549e1e6d02f580b31a538186e83140bd297fb47caf23387795a48468886e82562f661231890cb42637ecbe369dc4583d560055e94ca77
6
+ metadata.gz: 71deb80c6d970684e424fb8e4ca0a817399dc0b6564a7308093158a94ace0e24cac5d8e08a8b015327937b8dd45be25532dd8d9e02de0c3677f08297a08638dd
7
+ data.tar.gz: 79490a2f0db69914e01c69fdcf135cc3541609cfca29017154d3c33325c57f682c3d90bbfa0bca1a7f3d82e4cbe4af5259a77cf42861c142921491e21949bddf
data/History.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # Liquid Change Log
2
2
 
3
+ ## 4.0.2 / 2019-03-08
4
+
5
+ ### Changed
6
+ * Add `where` filter (#1026) [Samuel Doiron]
7
+ * Add `ParseTreeVisitor` to iterate the Liquid AST (#1025) [Stephen Paul Weber]
8
+ * Improve `strip_html` performance (#1032) [printercu]
9
+
10
+ ### Fixed
11
+ * Add error checking for invalid combinations of inputs to sort, sort_natural, where, uniq, map, compact filters (#1059) [Garland Zhang]
12
+ * Validate the character encoding in url_decode (#1070) [Clayton Smith]
13
+
14
+ ## 4.0.1 / 2018-10-09
15
+
16
+ ### Changed
17
+ * Add benchmark group in Gemfile (#855) [Jerry Liu]
18
+ * Allow benchmarks to benchmark render by itself (#851) [Jerry Liu]
19
+ * Avoid calling `line_number` on String node when rescuing a render error. (#860) [Dylan Thacker-Smith]
20
+ * Avoid duck typing to detect whether to call render on a node. [Dylan Thacker-Smith]
21
+ * Clarify spelling of `reversed` on `for` block tag (#843) [Mark Crossfield]
22
+ * Replace recursion with loop to avoid potential stack overflow from malicious input (#891, #892) [Dylan Thacker-Smith]
23
+ * Limit block tag nesting to 100 (#894) [Dylan Thacker-Smith]
24
+ * Replace `assert_equal nil` with `assert_nil` (#895) [Dylan Thacker-Smith]
25
+ * Remove Spy Gem (#896) [Dylan Thacker-Smith]
26
+ * Add `collection_name` and `variable_name` reader to `For` block (#909)
27
+ * Symbols render as strings (#920) [Justin Li]
28
+ * Remove default value from Hash objects (#932) [Maxime Bedard]
29
+ * Remove one level of nesting (#944) [Dylan Thacker-Smith]
30
+ * Update Rubocop version (#952) [Justin Li]
31
+ * Add `at_least` and `at_most` filters (#954, #958) [Nithin Bekal]
32
+ * Add a regression test for a liquid-c trim mode bug (#972) [Dylan Thacker-Smith]
33
+ * Use https rather than git protocol to fetch liquid-c [Dylan Thacker-Smith]
34
+ * Add tests against Ruby 2.4 (#963) and 2.5 (#981)
35
+ * Replace RegExp literals with constants (#988) [Ashwin Maroli]
36
+ * Replace unnecessary `#each_with_index` with `#each` (#992) [Ashwin Maroli]
37
+ * Improve the unexpected end delimiter message for block tags. (#1003) [Dylan Thacker-Smith]
38
+ * Refactor and optimize rendering (#1005) [Christopher Aue]
39
+ * Add installation instruction (#1006) [Ben Gift]
40
+ * Remove Circle CI (#1010)
41
+ * Rename deprecated `BigDecimal.new` to `BigDecimal` (#1024) [Koichi ITO]
42
+ * Rename deprecated Rubocop name (#1027) [Justin Li]
43
+
44
+ ### Fixed
45
+ * Handle `join` filter on non String joiners (#857) [Richard Monette]
46
+ * Fix duplicate inclusion condition logic error of `Liquid::Strainer.add_filter` method (#861)
47
+ * Fix `escape`, `url_encode`, `url_decode` not handling non-string values (#898) [Thierry Joyal]
48
+ * Fix raise when variable is defined but nil when using `strict_variables` [Pascal Betz]
49
+ * Fix `sort` and `sort_natural` to handle arrays with nils (#930) [Eric Chan]
50
+
3
51
  ## 4.0.0 / 2016-12-14 / branch "4-0-stable"
4
52
 
5
53
  ### Changed
@@ -45,6 +45,7 @@ module Liquid
45
45
  end
46
46
 
47
47
  require "liquid/version"
48
+ require 'liquid/parse_tree_visitor'
48
49
  require 'liquid/lexer'
49
50
  require 'liquid/parser'
50
51
  require 'liquid/i18n'
@@ -29,7 +29,7 @@ module Liquid
29
29
  @@operators
30
30
  end
31
31
 
32
- attr_reader :attachment
32
+ attr_reader :attachment, :child_condition
33
33
  attr_accessor :left, :operator, :right
34
34
 
35
35
  def initialize(left = nil, operator = nil, right = nil)
@@ -83,7 +83,7 @@ module Liquid
83
83
 
84
84
  protected
85
85
 
86
- attr_reader :child_relation, :child_condition
86
+ attr_reader :child_relation
87
87
 
88
88
  private
89
89
 
@@ -128,6 +128,15 @@ module Liquid
128
128
  end
129
129
  end
130
130
  end
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
131
140
  end
132
141
 
133
142
  class ElseCondition < Condition
@@ -19,7 +19,7 @@ module Liquid
19
19
  'false'.freeze => false,
20
20
  'blank'.freeze => MethodLiteral.new(:blank?, '').freeze,
21
21
  'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
22
- }
22
+ }.freeze
23
23
 
24
24
  SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
25
25
  DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
@@ -26,7 +26,7 @@ module Liquid
26
26
  def interpolate(name, vars)
27
27
  name.gsub(/%\{(\w+)\}/) do
28
28
  # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
29
- "#{vars[$1.to_sym]}"
29
+ (vars[$1.to_sym]).to_s
30
30
  end
31
31
  end
32
32
 
@@ -12,7 +12,7 @@ module Liquid
12
12
  ')'.freeze => :close_round,
13
13
  '?'.freeze => :question,
14
14
  '-'.freeze => :dash
15
- }
15
+ }.freeze
16
16
  IDENTIFIER = /[a-zA-Z_][\w-]*\??/
17
17
  SINGLE_STRING_LITERAL = /'[^\']*'/
18
18
  DOUBLE_STRING_LITERAL = /"[^\"]*"/
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class ParseTreeVisitor
5
+ def self.for(node, callbacks = Hash.new(proc {}))
6
+ if defined?(node.class::ParseTreeVisitor)
7
+ node.class::ParseTreeVisitor
8
+ else
9
+ self
10
+ end.new(node, callbacks)
11
+ end
12
+
13
+ def initialize(node, callbacks)
14
+ @node = node
15
+ @callbacks = callbacks
16
+ end
17
+
18
+ def add_callback_for(*classes, &block)
19
+ callback = block
20
+ callback = ->(node, _) { yield node } if block.arity.abs == 1
21
+ callback = ->(_, _) { yield } if block.arity.zero?
22
+ classes.each { |klass| @callbacks[klass] = callback }
23
+ self
24
+ end
25
+
26
+ def visit(context = nil)
27
+ children.map do |node|
28
+ item, new_context = @callbacks[node.class].call(node, context)
29
+ [
30
+ item,
31
+ ParseTreeVisitor.for(node, @callbacks).visit(new_context || context)
32
+ ]
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ def children
39
+ @node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
40
+ end
41
+ end
42
+ end
@@ -9,8 +9,14 @@ module Liquid
9
9
  '<'.freeze => '&lt;'.freeze,
10
10
  '"'.freeze => '&quot;'.freeze,
11
11
  "'".freeze => '&#39;'.freeze
12
- }
12
+ }.freeze
13
13
  HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
14
+ STRIP_HTML_BLOCKS = Regexp.union(
15
+ /<script.*?<\/script>/m,
16
+ /<!--.*?-->/m,
17
+ /<style.*?<\/style>/m
18
+ )
19
+ STRIP_HTML_TAGS = /<.*?>/m
14
20
 
15
21
  # Return the size of an array or of an string
16
22
  def size(input)
@@ -46,7 +52,12 @@ module Liquid
46
52
  end
47
53
 
48
54
  def url_decode(input)
49
- CGI.unescape(input.to_s) unless input.nil?
55
+ return if input.nil?
56
+
57
+ result = CGI.unescape(input.to_s)
58
+ raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
59
+
60
+ result
50
61
  end
51
62
 
52
63
  def slice(input, offset, length = nil)
@@ -103,7 +114,9 @@ module Liquid
103
114
 
104
115
  def strip_html(input)
105
116
  empty = ''.freeze
106
- input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty)
117
+ result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
118
+ result.gsub!(STRIP_HTML_TAGS, empty)
119
+ result
107
120
  end
108
121
 
109
122
  # Remove all newlines from the string
@@ -120,25 +133,18 @@ module Liquid
120
133
  # provide optional property with which to sort an array of hashes or drops
121
134
  def sort(input, property = nil)
122
135
  ary = InputIterator.new(input)
136
+
137
+ return [] if ary.empty?
138
+
123
139
  if property.nil?
124
140
  ary.sort do |a, b|
125
- if !a.nil? && !b.nil?
126
- a <=> b
127
- else
128
- a.nil? ? 1 : -1
129
- end
141
+ nil_safe_compare(a, b)
130
142
  end
131
- elsif ary.empty? # The next two cases assume a non-empty array.
132
- []
133
143
  elsif ary.all? { |el| el.respond_to?(:[]) }
134
- ary.sort do |a, b|
135
- a = a[property]
136
- b = b[property]
137
- if !a.nil? && !b.nil?
138
- a <=> b
139
- else
140
- a.nil? ? 1 : -1
141
- end
144
+ begin
145
+ ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
146
+ rescue TypeError
147
+ raise_property_error(property)
142
148
  end
143
149
  end
144
150
  end
@@ -148,25 +154,39 @@ module Liquid
148
154
  def sort_natural(input, property = nil)
149
155
  ary = InputIterator.new(input)
150
156
 
157
+ return [] if ary.empty?
158
+
151
159
  if property.nil?
152
160
  ary.sort do |a, b|
153
- if !a.nil? && !b.nil?
154
- a.to_s.casecmp(b.to_s)
155
- else
156
- a.nil? ? 1 : -1
157
- end
161
+ nil_safe_casecmp(a, b)
158
162
  end
159
- elsif ary.empty? # The next two cases assume a non-empty array.
160
- []
161
163
  elsif ary.all? { |el| el.respond_to?(:[]) }
162
- ary.sort do |a, b|
163
- a = a[property]
164
- b = b[property]
165
- if !a.nil? && !b.nil?
166
- a.to_s.casecmp(b.to_s)
167
- else
168
- a.nil? ? 1 : -1
169
- end
164
+ begin
165
+ ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
166
+ rescue TypeError
167
+ raise_property_error(property)
168
+ end
169
+ end
170
+ end
171
+
172
+ # Filter the elements of an array to those with a certain property value.
173
+ # By default the target is any truthy value.
174
+ def where(input, property, target_value = nil)
175
+ ary = InputIterator.new(input)
176
+
177
+ if ary.empty?
178
+ []
179
+ elsif ary.first.respond_to?(:[]) && target_value.nil?
180
+ begin
181
+ ary.select { |item| item[property] }
182
+ rescue TypeError
183
+ raise_property_error(property)
184
+ end
185
+ elsif ary.first.respond_to?(:[])
186
+ begin
187
+ ary.select { |item| item[property] == target_value }
188
+ rescue TypeError
189
+ raise_property_error(property)
170
190
  end
171
191
  end
172
192
  end
@@ -181,7 +201,11 @@ module Liquid
181
201
  elsif ary.empty? # The next two cases assume a non-empty array.
182
202
  []
183
203
  elsif ary.first.respond_to?(:[])
184
- ary.uniq{ |a| a[property] }
204
+ begin
205
+ ary.uniq { |a| a[property] }
206
+ rescue TypeError
207
+ raise_property_error(property)
208
+ end
185
209
  end
186
210
  end
187
211
 
@@ -203,6 +227,8 @@ module Liquid
203
227
  r.is_a?(Proc) ? r.call : r
204
228
  end
205
229
  end
230
+ rescue TypeError
231
+ raise_property_error(property)
206
232
  end
207
233
 
208
234
  # Remove nils within an array
@@ -215,7 +241,11 @@ module Liquid
215
241
  elsif ary.empty? # The next two cases assume a non-empty array.
216
242
  []
217
243
  elsif ary.first.respond_to?(:[])
218
- ary.reject{ |a| a[property].nil? }
244
+ begin
245
+ ary.reject { |a| a[property].nil? }
246
+ rescue TypeError
247
+ raise_property_error(property)
248
+ end
219
249
  end
220
250
  end
221
251
 
@@ -399,11 +429,31 @@ module Liquid
399
429
 
400
430
  private
401
431
 
432
+ def raise_property_error(property)
433
+ raise Liquid::ArgumentError.new("cannot select the property '#{property}'")
434
+ end
435
+
402
436
  def apply_operation(input, operand, operation)
403
437
  result = Utils.to_number(input).send(operation, Utils.to_number(operand))
404
438
  result.is_a?(BigDecimal) ? result.to_f : result
405
439
  end
406
440
 
441
+ def nil_safe_compare(a, b)
442
+ if !a.nil? && !b.nil?
443
+ a <=> b
444
+ else
445
+ a.nil? ? 1 : -1
446
+ end
447
+ end
448
+
449
+ def nil_safe_casecmp(a, b)
450
+ if !a.nil? && !b.nil?
451
+ a.to_s.casecmp(b.to_s)
452
+ else
453
+ a.nil? ? 1 : -1
454
+ end
455
+ end
456
+
407
457
  class InputIterator
408
458
  include Enumerable
409
459
 
@@ -10,6 +10,8 @@ module Liquid
10
10
  class Assign < Tag
11
11
  Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
12
12
 
13
+ attr_reader :to, :from
14
+
13
15
  def initialize(tag_name, markup, options)
14
16
  super
15
17
  if markup =~ Syntax
@@ -45,6 +47,12 @@ module Liquid
45
47
  1
46
48
  end
47
49
  end
50
+
51
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
52
+ def children
53
+ [@node.from]
54
+ end
55
+ end
48
56
  end
49
57
 
50
58
  Template.register_tag('assign'.freeze, Assign)
@@ -3,6 +3,8 @@ module Liquid
3
3
  Syntax = /(#{QuotedFragment})/o
4
4
  WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
5
5
 
6
+ attr_reader :blocks, :left
7
+
6
8
  def initialize(tag_name, markup, options)
7
9
  super
8
10
  @blocks = []
@@ -80,6 +82,12 @@ module Liquid
80
82
  block.attach(BlockBody.new)
81
83
  @blocks << block
82
84
  end
85
+
86
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
87
+ def children
88
+ [@node.left] + @node.blocks
89
+ end
90
+ end
83
91
  end
84
92
 
85
93
  Template.register_tag('case'.freeze, Case)
@@ -15,6 +15,8 @@ module Liquid
15
15
  SimpleSyntax = /\A#{QuotedFragment}+/o
16
16
  NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
17
17
 
18
+ attr_reader :variables
19
+
18
20
  def initialize(tag_name, markup, options)
19
21
  super
20
22
  case markup
@@ -51,6 +53,12 @@ module Liquid
51
53
  $1 ? Expression.parse($1) : nil
52
54
  end.compact
53
55
  end
56
+
57
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
58
+ def children
59
+ Array(@node.variables)
60
+ end
61
+ end
54
62
  end
55
63
 
56
64
  Template.register_tag('cycle', Cycle)
@@ -46,8 +46,7 @@ module Liquid
46
46
  class For < Block
47
47
  Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
48
48
 
49
- attr_reader :collection_name
50
- attr_reader :variable_name
49
+ attr_reader :collection_name, :variable_name, :limit, :from
51
50
 
52
51
  def initialize(tag_name, markup, options)
53
52
  super
@@ -192,6 +191,12 @@ module Liquid
192
191
  def render_else(context)
193
192
  @else_block ? @else_block.render(context) : ''.freeze
194
193
  end
194
+
195
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
196
+ def children
197
+ (super + [@node.limit, @node.from, @node.collection_name]).compact
198
+ end
199
+ end
195
200
  end
196
201
 
197
202
  Template.register_tag('for'.freeze, For)
@@ -12,7 +12,9 @@ module Liquid
12
12
  class If < Block
13
13
  Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
14
14
  ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
15
- BOOLEAN_OPERATORS = %w(and or)
15
+ BOOLEAN_OPERATORS = %w(and or).freeze
16
+
17
+ attr_reader :blocks
16
18
 
17
19
  def initialize(tag_name, markup, options)
18
20
  super
@@ -20,15 +22,15 @@ module Liquid
20
22
  push_block('if'.freeze, markup)
21
23
  end
22
24
 
25
+ def nodelist
26
+ @blocks.map(&:attachment)
27
+ end
28
+
23
29
  def parse(tokens)
24
30
  while parse_body(@blocks.last.attachment, tokens)
25
31
  end
26
32
  end
27
33
 
28
- def nodelist
29
- @blocks.map(&:attachment)
30
- end
31
-
32
34
  def unknown_tag(tag, markup, tokens)
33
35
  if ['elsif'.freeze, 'else'.freeze].include?(tag)
34
36
  push_block(tag, markup)
@@ -108,6 +110,12 @@ module Liquid
108
110
  Condition.new(a)
109
111
  end
110
112
  end
113
+
114
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
115
+ def children
116
+ @node.blocks
117
+ end
118
+ end
111
119
  end
112
120
 
113
121
  Template.register_tag('if'.freeze, If)
@@ -16,6 +16,8 @@ module Liquid
16
16
  class Include < Tag
17
17
  Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
18
18
 
19
+ attr_reader :template_name_expr, :variable_name_expr, :attributes
20
+
19
21
  def initialize(tag_name, markup, options)
20
22
  super
21
23
 
@@ -107,6 +109,15 @@ module Liquid
107
109
 
108
110
  file_system.read_template_file(context.evaluate(@template_name_expr))
109
111
  end
112
+
113
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
114
+ def children
115
+ [
116
+ @node.template_name_expr,
117
+ @node.variable_name_expr
118
+ ] + @node.attributes.values
119
+ end
120
+ end
110
121
  end
111
122
 
112
123
  Template.register_tag('include'.freeze, Include)
@@ -2,6 +2,8 @@ module Liquid
2
2
  class TableRow < Block
3
3
  Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
4
4
 
5
+ attr_reader :variable_name, :collection_name, :attributes
6
+
5
7
  def initialize(tag_name, markup, options)
6
8
  super
7
9
  if markup =~ Syntax
@@ -48,6 +50,12 @@ module Liquid
48
50
  result << "</tr>\n"
49
51
  result
50
52
  end
53
+
54
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
55
+ def children
56
+ super + @node.attributes.values + [@node.collection_name]
57
+ end
58
+ end
51
59
  end
52
60
 
53
61
  Template.register_tag('tablerow'.freeze, TableRow)
@@ -0,0 +1,5 @@
1
+ module Liquid
2
+ module Truffle
3
+
4
+ end
5
+ end
@@ -138,5 +138,11 @@ module Liquid
138
138
  raise error
139
139
  end
140
140
  end
141
+
142
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
143
+ def children
144
+ [@node.name] + @node.filters.flatten
145
+ end
146
+ end
141
147
  end
142
148
  end
@@ -1,7 +1,7 @@
1
1
  module Liquid
2
2
  class VariableLookup
3
3
  SQUARE_BRACKETED = /\A\[(.*)\]\z/m
4
- COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
4
+ COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze].freeze
5
5
 
6
6
  attr_reader :name, :lookups
7
7
 
@@ -78,5 +78,11 @@ module Liquid
78
78
  def state
79
79
  [@name, @lookups, @command_flags]
80
80
  end
81
+
82
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
83
+ def children
84
+ @node.lookups
85
+ end
86
+ end
81
87
  end
82
88
  end
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+
2
3
  module Liquid
3
- VERSION = "4.0.1"
4
+ VERSION = "4.0.2".freeze
4
5
  end
@@ -123,7 +123,7 @@ class ErrorHandlingTest < Minitest::Test
123
123
  ',
124
124
  error_mode: :warn,
125
125
  line_numbers: true
126
- )
126
+ )
127
127
 
128
128
  assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
129
129
  template.warnings.map(&:message)
@@ -140,7 +140,7 @@ class ErrorHandlingTest < Minitest::Test
140
140
  ',
141
141
  error_mode: :strict,
142
142
  line_numbers: true
143
- )
143
+ )
144
144
  end
145
145
 
146
146
  assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message
@@ -158,7 +158,7 @@ class ErrorHandlingTest < Minitest::Test
158
158
  bla
159
159
  ',
160
160
  line_numbers: true
161
- )
161
+ )
162
162
  end
163
163
 
164
164
  assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class ParseTreeVisitorTest < Minitest::Test
6
+ include Liquid
7
+
8
+ def test_variable
9
+ assert_equal(
10
+ ["test"],
11
+ visit(%({{ test }}))
12
+ )
13
+ end
14
+
15
+ def test_varible_with_filter
16
+ assert_equal(
17
+ ["test", "infilter"],
18
+ visit(%({{ test | split: infilter }}))
19
+ )
20
+ end
21
+
22
+ def test_dynamic_variable
23
+ assert_equal(
24
+ ["test", "inlookup"],
25
+ visit(%({{ test[inlookup] }}))
26
+ )
27
+ end
28
+
29
+ def test_if_condition
30
+ assert_equal(
31
+ ["test"],
32
+ visit(%({% if test %}{% endif %}))
33
+ )
34
+ end
35
+
36
+ def test_complex_if_condition
37
+ assert_equal(
38
+ ["test"],
39
+ visit(%({% if 1 == 1 and 2 == test %}{% endif %}))
40
+ )
41
+ end
42
+
43
+ def test_if_body
44
+ assert_equal(
45
+ ["test"],
46
+ visit(%({% if 1 == 1 %}{{ test }}{% endif %}))
47
+ )
48
+ end
49
+
50
+ def test_unless_condition
51
+ assert_equal(
52
+ ["test"],
53
+ visit(%({% unless test %}{% endunless %}))
54
+ )
55
+ end
56
+
57
+ def test_complex_unless_condition
58
+ assert_equal(
59
+ ["test"],
60
+ visit(%({% unless 1 == 1 and 2 == test %}{% endunless %}))
61
+ )
62
+ end
63
+
64
+ def test_unless_body
65
+ assert_equal(
66
+ ["test"],
67
+ visit(%({% unless 1 == 1 %}{{ test }}{% endunless %}))
68
+ )
69
+ end
70
+
71
+ def test_elsif_condition
72
+ assert_equal(
73
+ ["test"],
74
+ visit(%({% if 1 == 1 %}{% elsif test %}{% endif %}))
75
+ )
76
+ end
77
+
78
+ def test_complex_elsif_condition
79
+ assert_equal(
80
+ ["test"],
81
+ visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %}))
82
+ )
83
+ end
84
+
85
+ def test_elsif_body
86
+ assert_equal(
87
+ ["test"],
88
+ visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %}))
89
+ )
90
+ end
91
+
92
+ def test_else_body
93
+ assert_equal(
94
+ ["test"],
95
+ visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %}))
96
+ )
97
+ end
98
+
99
+ def test_case_left
100
+ assert_equal(
101
+ ["test"],
102
+ visit(%({% case test %}{% endcase %}))
103
+ )
104
+ end
105
+
106
+ def test_case_condition
107
+ assert_equal(
108
+ ["test"],
109
+ visit(%({% case 1 %}{% when test %}{% endcase %}))
110
+ )
111
+ end
112
+
113
+ def test_case_when_body
114
+ assert_equal(
115
+ ["test"],
116
+ visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %}))
117
+ )
118
+ end
119
+
120
+ def test_case_else_body
121
+ assert_equal(
122
+ ["test"],
123
+ visit(%({% case 1 %}{% else %}{{ test }}{% endcase %}))
124
+ )
125
+ end
126
+
127
+ def test_for_in
128
+ assert_equal(
129
+ ["test"],
130
+ visit(%({% for x in test %}{% endfor %}))
131
+ )
132
+ end
133
+
134
+ def test_for_limit
135
+ assert_equal(
136
+ ["test"],
137
+ visit(%({% for x in (1..5) limit: test %}{% endfor %}))
138
+ )
139
+ end
140
+
141
+ def test_for_offset
142
+ assert_equal(
143
+ ["test"],
144
+ visit(%({% for x in (1..5) offset: test %}{% endfor %}))
145
+ )
146
+ end
147
+
148
+ def test_for_body
149
+ assert_equal(
150
+ ["test"],
151
+ visit(%({% for x in (1..5) %}{{ test }}{% endfor %}))
152
+ )
153
+ end
154
+
155
+ def test_tablerow_in
156
+ assert_equal(
157
+ ["test"],
158
+ visit(%({% tablerow x in test %}{% endtablerow %}))
159
+ )
160
+ end
161
+
162
+ def test_tablerow_limit
163
+ assert_equal(
164
+ ["test"],
165
+ visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %}))
166
+ )
167
+ end
168
+
169
+ def test_tablerow_offset
170
+ assert_equal(
171
+ ["test"],
172
+ visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %}))
173
+ )
174
+ end
175
+
176
+ def test_tablerow_body
177
+ assert_equal(
178
+ ["test"],
179
+ visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %}))
180
+ )
181
+ end
182
+
183
+ def test_cycle
184
+ assert_equal(
185
+ ["test"],
186
+ visit(%({% cycle test %}))
187
+ )
188
+ end
189
+
190
+ def test_assign
191
+ assert_equal(
192
+ ["test"],
193
+ visit(%({% assign x = test %}))
194
+ )
195
+ end
196
+
197
+ def test_capture
198
+ assert_equal(
199
+ ["test"],
200
+ visit(%({% capture x %}{{ test }}{% endcapture %}))
201
+ )
202
+ end
203
+
204
+ def test_include
205
+ assert_equal(
206
+ ["test"],
207
+ visit(%({% include test %}))
208
+ )
209
+ end
210
+
211
+ def test_include_with
212
+ assert_equal(
213
+ ["test"],
214
+ visit(%({% include "hai" with test %}))
215
+ )
216
+ end
217
+
218
+ def test_include_for
219
+ assert_equal(
220
+ ["test"],
221
+ visit(%({% include "hai" for test %}))
222
+ )
223
+ end
224
+
225
+ def test_preserve_tree_structure
226
+ assert_equal(
227
+ [[nil, [
228
+ [nil, [[nil, [["other", []]]]]],
229
+ ["test", []],
230
+ ["xs", []]
231
+ ]]],
232
+ traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit
233
+ )
234
+ end
235
+
236
+ private
237
+
238
+ def traversal(template)
239
+ ParseTreeVisitor
240
+ .for(Template.parse(template).root)
241
+ .add_callback_for(VariableLookup, &:name)
242
+ end
243
+
244
+ def visit(template)
245
+ traversal(template).visit.flatten.compact
246
+ end
247
+ end
@@ -99,7 +99,7 @@ class ParsingQuirksTest < Minitest::Test
99
99
  # After the messed up quotes a filter without parameters (reverse) should work
100
100
  # but one with parameters (remove) shouldn't be detected.
101
101
  assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}")
102
- assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
102
+ assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
103
103
  end
104
104
  end
105
105
 
@@ -158,6 +158,10 @@ class StandardFiltersTest < Minitest::Test
158
158
  assert_equal '1', @filters.url_decode(1)
159
159
  assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))
160
160
  assert_nil @filters.url_decode(nil)
161
+ exception = assert_raises Liquid::ArgumentError do
162
+ @filters.url_decode('%ff')
163
+ end
164
+ assert_equal 'Liquid error: invalid byte sequence in UTF-8', exception.message
161
165
  end
162
166
 
163
167
  def test_truncatewords
@@ -177,6 +181,9 @@ class StandardFiltersTest < Minitest::Test
177
181
  assert_equal 'test', @filters.strip_html("<div\nclass='multiline'>test</div>")
178
182
  assert_equal 'test', @filters.strip_html("<!-- foo bar \n test -->test")
179
183
  assert_equal '', @filters.strip_html(nil)
184
+
185
+ # Quirk of the existing implementation
186
+ assert_equal 'foo;', @filters.strip_html("<<<script </script>script>foo;</script>")
180
187
  end
181
188
 
182
189
  def test_join
@@ -268,10 +275,34 @@ class StandardFiltersTest < Minitest::Test
268
275
  assert_equal [], @filters.sort([], "a")
269
276
  end
270
277
 
278
+ def test_sort_invalid_property
279
+ foo = [
280
+ [1],
281
+ [2],
282
+ [3]
283
+ ]
284
+
285
+ assert_raises Liquid::ArgumentError do
286
+ @filters.sort(foo, "bar")
287
+ end
288
+ end
289
+
271
290
  def test_sort_natural_empty_array
272
291
  assert_equal [], @filters.sort_natural([], "a")
273
292
  end
274
293
 
294
+ def test_sort_natural_invalid_property
295
+ foo = [
296
+ [1],
297
+ [2],
298
+ [3]
299
+ ]
300
+
301
+ assert_raises Liquid::ArgumentError do
302
+ @filters.sort_natural(foo, "bar")
303
+ end
304
+ end
305
+
275
306
  def test_legacy_sort_hash
276
307
  assert_equal [{ a: 1, b: 2 }], @filters.sort({ a: 1, b: 2 })
277
308
  end
@@ -295,10 +326,34 @@ class StandardFiltersTest < Minitest::Test
295
326
  assert_equal [], @filters.uniq([], "a")
296
327
  end
297
328
 
329
+ def test_uniq_invalid_property
330
+ foo = [
331
+ [1],
332
+ [2],
333
+ [3]
334
+ ]
335
+
336
+ assert_raises Liquid::ArgumentError do
337
+ @filters.uniq(foo, "bar")
338
+ end
339
+ end
340
+
298
341
  def test_compact_empty_array
299
342
  assert_equal [], @filters.compact([], "a")
300
343
  end
301
344
 
345
+ def test_compact_invalid_property
346
+ foo = [
347
+ [1],
348
+ [2],
349
+ [3]
350
+ ]
351
+
352
+ assert_raises Liquid::ArgumentError do
353
+ @filters.compact(foo, "bar")
354
+ end
355
+ end
356
+
302
357
  def test_reverse
303
358
  assert_equal [4, 3, 2, 1], @filters.reverse([1, 2, 3, 4])
304
359
  end
@@ -364,6 +419,29 @@ class StandardFiltersTest < Minitest::Test
364
419
  assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new
365
420
  end
366
421
 
422
+ def test_map_returns_empty_on_2d_input_array
423
+ foo = [
424
+ [1],
425
+ [2],
426
+ [3]
427
+ ]
428
+
429
+ assert_raises Liquid::ArgumentError do
430
+ @filters.map(foo, "bar")
431
+ end
432
+ end
433
+
434
+ def test_map_returns_empty_with_no_property
435
+ foo = [
436
+ [1],
437
+ [2],
438
+ [3]
439
+ ]
440
+ assert_raises Liquid::ArgumentError do
441
+ @filters.map(foo, nil)
442
+ end
443
+ end
444
+
367
445
  def test_sort_works_on_enumerables
368
446
  assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new
369
447
  end
@@ -394,9 +472,9 @@ class StandardFiltersTest < Minitest::Test
394
472
  assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y")
395
473
 
396
474
  assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y")
397
- assert_equal "#{Date.today.year}", @filters.date('now', '%Y')
398
- assert_equal "#{Date.today.year}", @filters.date('today', '%Y')
399
- assert_equal "#{Date.today.year}", @filters.date('Today', '%Y')
475
+ assert_equal Date.today.year.to_s, @filters.date('now', '%Y')
476
+ assert_equal Date.today.year.to_s, @filters.date('today', '%Y')
477
+ assert_equal Date.today.year.to_s, @filters.date('Today', '%Y')
400
478
 
401
479
  assert_nil @filters.date(nil, "%B")
402
480
 
@@ -614,6 +692,78 @@ class StandardFiltersTest < Minitest::Test
614
692
  assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
615
693
  end
616
694
 
695
+ def test_where
696
+ input = [
697
+ { "handle" => "alpha", "ok" => true },
698
+ { "handle" => "beta", "ok" => false },
699
+ { "handle" => "gamma", "ok" => false },
700
+ { "handle" => "delta", "ok" => true }
701
+ ]
702
+
703
+ expectation = [
704
+ { "handle" => "alpha", "ok" => true },
705
+ { "handle" => "delta", "ok" => true }
706
+ ]
707
+
708
+ assert_equal expectation, @filters.where(input, "ok", true)
709
+ assert_equal expectation, @filters.where(input, "ok")
710
+ end
711
+
712
+ def test_where_no_key_set
713
+ input = [
714
+ { "handle" => "alpha", "ok" => true },
715
+ { "handle" => "beta" },
716
+ { "handle" => "gamma" },
717
+ { "handle" => "delta", "ok" => true }
718
+ ]
719
+
720
+ expectation = [
721
+ { "handle" => "alpha", "ok" => true },
722
+ { "handle" => "delta", "ok" => true }
723
+ ]
724
+
725
+ assert_equal expectation, @filters.where(input, "ok", true)
726
+ assert_equal expectation, @filters.where(input, "ok")
727
+ end
728
+
729
+ def test_where_non_array_map_input
730
+ assert_equal [{ "a" => "ok" }], @filters.where({ "a" => "ok" }, "a", "ok")
731
+ assert_equal [], @filters.where({ "a" => "not ok" }, "a", "ok")
732
+ end
733
+
734
+ def test_where_indexable_but_non_map_value
735
+ assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok", true) }
736
+ assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok") }
737
+ end
738
+
739
+ def test_where_non_boolean_value
740
+ input = [
741
+ { "message" => "Bonjour!", "language" => "French" },
742
+ { "message" => "Hello!", "language" => "English" },
743
+ { "message" => "Hallo!", "language" => "German" }
744
+ ]
745
+
746
+ assert_equal [{ "message" => "Bonjour!", "language" => "French" }], @filters.where(input, "language", "French")
747
+ assert_equal [{ "message" => "Hallo!", "language" => "German" }], @filters.where(input, "language", "German")
748
+ assert_equal [{ "message" => "Hello!", "language" => "English" }], @filters.where(input, "language", "English")
749
+ end
750
+
751
+ def test_where_array_of_only_unindexable_values
752
+ assert_nil @filters.where([nil], "ok", true)
753
+ assert_nil @filters.where([nil], "ok")
754
+ end
755
+
756
+ def test_where_no_target_value
757
+ input = [
758
+ { "foo" => false },
759
+ { "foo" => true },
760
+ { "foo" => "for sure" },
761
+ { "bar" => true }
762
+ ]
763
+
764
+ assert_equal [{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo")
765
+ end
766
+
617
767
  private
618
768
 
619
769
  def with_timezone(tz)
File without changes
@@ -0,0 +1,9 @@
1
+ require 'test_helper'
2
+
3
+ class TruffleTest < Minitest::Test
4
+ include Liquid
5
+
6
+ def test_truffle_works
7
+
8
+ end
9
+ end
@@ -24,9 +24,9 @@ class ConditionUnitTest < Minitest::Test
24
24
  assert_evaluates_true 1, '<=', 1
25
25
  # negative numbers
26
26
  assert_evaluates_true 1, '>', -1
27
- assert_evaluates_true (-1), '<', 1
27
+ assert_evaluates_true -1, '<', 1
28
28
  assert_evaluates_true 1.0, '>', -1.0
29
- assert_evaluates_true (-1.0), '<', 1.0
29
+ assert_evaluates_true -1.0, '<', 1.0
30
30
  end
31
31
 
32
32
  def test_default_operators_evalute_false
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: liquid
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.1
4
+ version: 4.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Lütke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-09 00:00:00.000000000 Z
11
+ date: 2019-03-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -67,6 +67,7 @@ files:
67
67
  - lib/liquid/lexer.rb
68
68
  - lib/liquid/locales/en.yml
69
69
  - lib/liquid/parse_context.rb
70
+ - lib/liquid/parse_tree_visitor.rb
70
71
  - lib/liquid/parser.rb
71
72
  - lib/liquid/parser_switching.rb
72
73
  - lib/liquid/profiler.rb
@@ -95,6 +96,7 @@ files:
95
96
  - lib/liquid/tags/unless.rb
96
97
  - lib/liquid/template.rb
97
98
  - lib/liquid/tokenizer.rb
99
+ - lib/liquid/truffle.rb
98
100
  - lib/liquid/utils.rb
99
101
  - lib/liquid/variable.rb
100
102
  - lib/liquid/variable_lookup.rb
@@ -111,6 +113,7 @@ files:
111
113
  - test/integration/filter_test.rb
112
114
  - test/integration/hash_ordering_test.rb
113
115
  - test/integration/output_test.rb
116
+ - test/integration/parse_tree_visitor_test.rb
114
117
  - test/integration/parsing_quirks_test.rb
115
118
  - test/integration/render_profiling_test.rb
116
119
  - test/integration/security_test.rb
@@ -130,6 +133,7 @@ files:
130
133
  - test/integration/trim_mode_test.rb
131
134
  - test/integration/variable_test.rb
132
135
  - test/test_helper.rb
136
+ - test/truffle/truffle_test.rb
133
137
  - test/unit/block_unit_test.rb
134
138
  - test/unit/condition_unit_test.rb
135
139
  - test/unit/context_unit_test.rb
@@ -165,8 +169,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
165
169
  - !ruby/object:Gem::Version
166
170
  version: 1.3.7
167
171
  requirements: []
168
- rubyforge_project:
169
- rubygems_version: 2.7.6
172
+ rubygems_version: 3.0.2
170
173
  signing_key:
171
174
  specification_version: 4
172
175
  summary: A secure, non-evaling end user template engine with aesthetic markup.
@@ -191,6 +194,7 @@ test_files:
191
194
  - test/integration/hash_ordering_test.rb
192
195
  - test/integration/variable_test.rb
193
196
  - test/integration/blank_test.rb
197
+ - test/integration/parse_tree_visitor_test.rb
194
198
  - test/integration/assign_test.rb
195
199
  - test/integration/trim_mode_test.rb
196
200
  - test/integration/context_test.rb
@@ -216,5 +220,6 @@ test_files:
216
220
  - test/integration/render_profiling_test.rb
217
221
  - test/integration/parsing_quirks_test.rb
218
222
  - test/integration/filter_test.rb
223
+ - test/truffle/truffle_test.rb
219
224
  - test/fixtures/en_locale.yml
220
225
  - test/test_helper.rb