liquid 3.0.6 → 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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +98 -58
  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 +50 -46
  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_switching.rb +4 -4
  21. data/lib/liquid/profiler/hooks.rb +7 -7
  22. data/lib/liquid/profiler.rb +18 -19
  23. data/lib/liquid/range_lookup.rb +16 -1
  24. data/lib/liquid/resource_limits.rb +23 -0
  25. data/lib/liquid/standardfilters.rb +121 -61
  26. data/lib/liquid/strainer.rb +14 -7
  27. data/lib/liquid/tablerowloop_drop.rb +62 -0
  28. data/lib/liquid/tag.rb +9 -8
  29. data/lib/liquid/tags/assign.rb +17 -4
  30. data/lib/liquid/tags/break.rb +0 -3
  31. data/lib/liquid/tags/capture.rb +1 -1
  32. data/lib/liquid/tags/case.rb +19 -12
  33. data/lib/liquid/tags/comment.rb +2 -2
  34. data/lib/liquid/tags/cycle.rb +6 -6
  35. data/lib/liquid/tags/decrement.rb +1 -4
  36. data/lib/liquid/tags/for.rb +95 -75
  37. data/lib/liquid/tags/if.rb +49 -44
  38. data/lib/liquid/tags/ifchanged.rb +0 -2
  39. data/lib/liquid/tags/include.rb +61 -52
  40. data/lib/liquid/tags/raw.rb +32 -4
  41. data/lib/liquid/tags/table_row.rb +12 -30
  42. data/lib/liquid/tags/unless.rb +3 -4
  43. data/lib/liquid/template.rb +42 -54
  44. data/lib/liquid/tokenizer.rb +31 -0
  45. data/lib/liquid/utils.rb +52 -8
  46. data/lib/liquid/variable.rb +46 -45
  47. data/lib/liquid/variable_lookup.rb +7 -5
  48. data/lib/liquid/version.rb +1 -1
  49. data/lib/liquid.rb +9 -7
  50. data/test/integration/assign_test.rb +8 -8
  51. data/test/integration/blank_test.rb +14 -14
  52. data/test/integration/context_test.rb +2 -2
  53. data/test/integration/document_test.rb +19 -0
  54. data/test/integration/drop_test.rb +42 -40
  55. data/test/integration/error_handling_test.rb +99 -46
  56. data/test/integration/filter_test.rb +60 -20
  57. data/test/integration/hash_ordering_test.rb +9 -9
  58. data/test/integration/output_test.rb +26 -27
  59. data/test/integration/parsing_quirks_test.rb +15 -13
  60. data/test/integration/render_profiling_test.rb +20 -20
  61. data/test/integration/security_test.rb +9 -7
  62. data/test/integration/standard_filter_test.rb +179 -40
  63. data/test/integration/tags/break_tag_test.rb +1 -2
  64. data/test/integration/tags/continue_tag_test.rb +0 -1
  65. data/test/integration/tags/for_tag_test.rb +133 -98
  66. data/test/integration/tags/if_else_tag_test.rb +75 -77
  67. data/test/integration/tags/include_tag_test.rb +34 -30
  68. data/test/integration/tags/increment_tag_test.rb +10 -11
  69. data/test/integration/tags/raw_tag_test.rb +7 -1
  70. data/test/integration/tags/standard_tag_test.rb +121 -122
  71. data/test/integration/tags/statements_test.rb +3 -5
  72. data/test/integration/tags/table_row_test.rb +20 -19
  73. data/test/integration/tags/unless_else_tag_test.rb +6 -6
  74. data/test/integration/template_test.rb +190 -49
  75. data/test/integration/trim_mode_test.rb +525 -0
  76. data/test/integration/variable_test.rb +23 -13
  77. data/test/test_helper.rb +33 -5
  78. data/test/unit/block_unit_test.rb +8 -5
  79. data/test/unit/condition_unit_test.rb +86 -77
  80. data/test/unit/context_unit_test.rb +48 -57
  81. data/test/unit/file_system_unit_test.rb +3 -3
  82. data/test/unit/i18n_unit_test.rb +2 -2
  83. data/test/unit/lexer_unit_test.rb +11 -8
  84. data/test/unit/parser_unit_test.rb +2 -2
  85. data/test/unit/regexp_unit_test.rb +1 -1
  86. data/test/unit/strainer_unit_test.rb +80 -1
  87. data/test/unit/tag_unit_test.rb +7 -2
  88. data/test/unit/tags/case_tag_unit_test.rb +1 -1
  89. data/test/unit/tags/for_tag_unit_test.rb +2 -2
  90. data/test/unit/tags/if_tag_unit_test.rb +1 -1
  91. data/test/unit/template_unit_test.rb +14 -5
  92. data/test/unit/tokenizer_unit_test.rb +24 -7
  93. data/test/unit/variable_unit_test.rb +60 -43
  94. metadata +19 -14
  95. data/lib/liquid/module_ex.rb +0 -62
  96. data/lib/liquid/token.rb +0 -18
  97. data/test/unit/module_ex_unit_test.rb +0 -87
  98. /data/{MIT-LICENSE → LICENSE} +0 -0
data/lib/liquid/i18n.rb CHANGED
@@ -2,10 +2,9 @@ require 'yaml'
2
2
 
3
3
  module Liquid
4
4
  class I18n
5
- DEFAULT_LOCALE = File.join(File.expand_path(File.dirname(__FILE__)), "locales", "en.yml")
5
+ DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml")
6
6
 
7
- class TranslationError < StandardError
8
- end
7
+ TranslationError = Class.new(StandardError)
9
8
 
10
9
  attr_reader :path
11
10
 
@@ -23,11 +22,12 @@ module Liquid
23
22
  end
24
23
 
25
24
  private
25
+
26
26
  def interpolate(name, vars)
27
- name.gsub(/%\{(\w+)\}/) {
27
+ name.gsub(/%\{(\w+)\}/) do
28
28
  # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
29
29
  "#{vars[$1.to_sym]}"
30
- }
30
+ end
31
31
  end
32
32
 
33
33
  def deep_fetch_translation(name)
@@ -1,10 +1,9 @@
1
1
  module Liquid
2
-
3
2
  # An interrupt is any command that breaks processing of a block (ex: a for loop).
4
3
  class Interrupt
5
4
  attr_reader :message
6
5
 
7
- def initialize(message=nil)
6
+ def initialize(message = nil)
8
7
  @message = message || "interrupt".freeze
9
8
  end
10
9
  end
data/lib/liquid/lexer.rb CHANGED
@@ -9,9 +9,11 @@ module Liquid
9
9
  '['.freeze => :open_square,
10
10
  ']'.freeze => :close_square,
11
11
  '('.freeze => :open_round,
12
- ')'.freeze => :close_round
12
+ ')'.freeze => :close_round,
13
+ '?'.freeze => :question,
14
+ '-'.freeze => :dash
13
15
  }
14
- IDENTIFIER = /[\w\-?!]+/
16
+ IDENTIFIER = /[a-zA-Z_][\w-]*\??/
15
17
  SINGLE_STRING_LITERAL = /'[^\']*'/
16
18
  DOUBLE_STRING_LITERAL = /"[^\"]*"/
17
19
  NUMBER_LITERAL = /-?\d+(\.\d+)?/
@@ -25,7 +27,7 @@ module Liquid
25
27
  def tokenize
26
28
  @output = []
27
29
 
28
- while !@ss.eos?
30
+ until @ss.eos?
29
31
  @ss.skip(/\s*/)
30
32
  tok = case
31
33
  when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
@@ -37,7 +39,7 @@ module Liquid
37
39
  else
38
40
  c = @ss.getch
39
41
  if s = SPECIALS[c]
40
- [s,c]
42
+ [s, c]
41
43
  else
42
44
  raise SyntaxError, "Unexpected character #{c}"
43
45
  end
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  errors:
3
3
  syntax:
4
+ tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
4
5
  assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
5
6
  capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
6
7
  case: "Syntax Error in 'case' - Valid syntax: case [condition]"
@@ -14,9 +15,12 @@
14
15
  include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
15
16
  unknown_tag: "Unknown tag '%{tag}'"
16
17
  invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
17
- unexpected_else: "%{block_name} tag does not expect else tag"
18
+ unexpected_else: "%{block_name} tag does not expect 'else' tag"
19
+ unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
18
20
  tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
19
21
  variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
20
22
  tag_never_closed: "'%{block_name}' tag was never closed"
21
23
  meta_syntax_error: "Liquid syntax error: #{e.message}"
22
24
  table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
25
+ argument:
26
+ include: "Argument error in tag 'include' - Illegal template name"
@@ -0,0 +1,37 @@
1
+ module Liquid
2
+ class ParseContext
3
+ attr_accessor :locale, :line_number, :trim_whitespace
4
+ attr_reader :partial, :warnings, :error_mode
5
+
6
+ def initialize(options = {})
7
+ @template_options = options ? options.dup : {}
8
+ @locale = @template_options[:locale] ||= I18n.new
9
+ @warnings = []
10
+ self.partial = false
11
+ end
12
+
13
+ def [](option_key)
14
+ @options[option_key]
15
+ end
16
+
17
+ def partial=(value)
18
+ @partial = value
19
+ @options = value ? partial_options : @template_options
20
+ @error_mode = @options[:error_mode] || Template.error_mode
21
+ value
22
+ end
23
+
24
+ def partial_options
25
+ @partial_options ||= begin
26
+ dont_pass = @template_options[:include_options_blacklist]
27
+ if dont_pass == true
28
+ { locale: locale }
29
+ elsif dont_pass.is_a?(Array)
30
+ @template_options.reject { |k, v| dont_pass.include?(k) }
31
+ else
32
+ @template_options
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,25 +1,25 @@
1
1
  module Liquid
2
2
  module ParserSwitching
3
3
  def parse_with_selected_parser(markup)
4
- case @options[:error_mode] || Template.error_mode
4
+ case parse_context.error_mode
5
5
  when :strict then strict_parse_with_error_context(markup)
6
6
  when :lax then lax_parse(markup)
7
7
  when :warn
8
8
  begin
9
9
  return strict_parse_with_error_context(markup)
10
10
  rescue SyntaxError => e
11
- e.set_line_number_from_token(markup)
12
- @warnings ||= []
13
- @warnings << e
11
+ parse_context.warnings << e
14
12
  return lax_parse(markup)
15
13
  end
16
14
  end
17
15
  end
18
16
 
19
17
  private
18
+
20
19
  def strict_parse_with_error_context(markup)
21
20
  strict_parse(markup)
22
21
  rescue SyntaxError => e
22
+ e.line_number = line_number
23
23
  e.markup_context = markup_context(markup)
24
24
  raise e
25
25
  end
@@ -1,18 +1,18 @@
1
1
  module Liquid
2
- class Block < Tag
3
- def render_token_with_profiling(token, context)
4
- Profiler.profile_token_render(token) do
5
- render_token_without_profiling(token, context)
2
+ class BlockBody
3
+ def render_node_with_profiling(node, context)
4
+ Profiler.profile_node_render(node) do
5
+ render_node_without_profiling(node, context)
6
6
  end
7
7
  end
8
8
 
9
- alias_method :render_token_without_profiling, :render_token
10
- alias_method :render_token, :render_token_with_profiling
9
+ alias_method :render_node_without_profiling, :render_node
10
+ alias_method :render_node, :render_node_with_profiling
11
11
  end
12
12
 
13
13
  class Include < Tag
14
14
  def render_with_profiling(context)
15
- Profiler.profile_children(@template_name) do
15
+ Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
16
16
  render_without_profiling(context)
17
17
  end
18
18
  end
@@ -1,9 +1,11 @@
1
- module Liquid
1
+ require 'liquid/profiler/hooks'
2
2
 
3
+ module Liquid
3
4
  # Profiler enables support for profiling template rendering to help track down performance issues.
4
5
  #
5
- # To enable profiling, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>. Then, after
6
- # <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
6
+ # To enable profiling, first require 'liquid/profiler'.
7
+ # Then, to profile a parse/render cycle, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>.
8
+ # After <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
7
9
  # class via the <tt>Liquid::Template#profiler</tt> method.
8
10
  #
9
11
  # template = Liquid::Template.parse(template_content, profile: true)
@@ -17,7 +19,7 @@ module Liquid
17
19
  # inside of <tt>{% include %}</tt> tags.
18
20
  #
19
21
  # profile.each do |node|
20
- # # Access to the token itself
22
+ # # Access to the node itself
21
23
  # node.code
22
24
  #
23
25
  # # Which template and line number of this node.
@@ -44,17 +46,15 @@ module Liquid
44
46
  class Timing
45
47
  attr_reader :code, :partial, :line_number, :children
46
48
 
47
- def initialize(token, partial)
48
- @code = token.respond_to?(:raw) ? token.raw : token
49
+ def initialize(node, partial)
50
+ @code = node.respond_to?(:raw) ? node.raw : node
49
51
  @partial = partial
50
- @line_number = token.respond_to?(:line_number) ? token.line_number : nil
52
+ @line_number = node.respond_to?(:line_number) ? node.line_number : nil
51
53
  @children = []
52
54
  end
53
55
 
54
- def self.start(token, partial)
55
- new(token, partial).tap do |t|
56
- t.start
57
- end
56
+ def self.start(node, partial)
57
+ new(node, partial).tap(&:start)
58
58
  end
59
59
 
60
60
  def start
@@ -70,11 +70,11 @@ module Liquid
70
70
  end
71
71
  end
72
72
 
73
- def self.profile_token_render(token)
74
- if Profiler.current_profile && token.respond_to?(:render)
75
- Profiler.current_profile.start_token(token)
73
+ def self.profile_node_render(node)
74
+ if Profiler.current_profile && node.respond_to?(:render)
75
+ Profiler.current_profile.start_node(node)
76
76
  output = yield
77
- Profiler.current_profile.end_token(token)
77
+ Profiler.current_profile.end_node(node)
78
78
  output
79
79
  else
80
80
  yield
@@ -132,11 +132,11 @@ module Liquid
132
132
  @root_timing.children.length
133
133
  end
134
134
 
135
- def start_token(token)
136
- @timing_stack.push(Timing.start(token, current_partial))
135
+ def start_node(node)
136
+ @timing_stack.push(Timing.start(node, current_partial))
137
137
  end
138
138
 
139
- def end_token(token)
139
+ def end_node(_node)
140
140
  timing = @timing_stack.pop
141
141
  timing.finish
142
142
 
@@ -154,6 +154,5 @@ module Liquid
154
154
  def pop_partial
155
155
  @partial_stack.pop
156
156
  end
157
-
158
157
  end
159
158
  end
@@ -16,7 +16,22 @@ module Liquid
16
16
  end
17
17
 
18
18
  def evaluate(context)
19
- context.evaluate(@start_obj).to_i..context.evaluate(@end_obj).to_i
19
+ start_int = to_integer(context.evaluate(@start_obj))
20
+ end_int = to_integer(context.evaluate(@end_obj))
21
+ start_int..end_int
22
+ end
23
+
24
+ private
25
+
26
+ def to_integer(input)
27
+ case input
28
+ when Integer
29
+ input
30
+ when NilClass, String
31
+ input.to_i
32
+ else
33
+ Utils.to_integer(input)
34
+ end
20
35
  end
21
36
  end
22
37
  end
@@ -0,0 +1,23 @@
1
+ module Liquid
2
+ class ResourceLimits
3
+ attr_accessor :render_length, :render_score, :assign_score,
4
+ :render_length_limit, :render_score_limit, :assign_score_limit
5
+
6
+ def initialize(limits)
7
+ @render_length_limit = limits[:render_length_limit]
8
+ @render_score_limit = limits[:render_score_limit]
9
+ @assign_score_limit = limits[:assign_score_limit]
10
+ reset
11
+ end
12
+
13
+ def reached?
14
+ (@render_length_limit && @render_length > @render_length_limit) ||
15
+ (@render_score_limit && @render_score > @render_score_limit) ||
16
+ (@assign_score_limit && @assign_score > @assign_score_limit)
17
+ end
18
+
19
+ def reset
20
+ @render_length = @render_score = @assign_score = 0
21
+ end
22
+ end
23
+ end
@@ -2,7 +2,6 @@ require 'cgi'
2
2
  require 'bigdecimal'
3
3
 
4
4
  module Liquid
5
-
6
5
  module StandardFilters
7
6
  HTML_ESCAPE = {
8
7
  '&'.freeze => '&amp;'.freeze,
@@ -34,7 +33,7 @@ module Liquid
34
33
  end
35
34
 
36
35
  def escape(input)
37
- CGI.escapeHTML(input).untaint rescue input
36
+ CGI.escapeHTML(input).untaint unless input.nil?
38
37
  end
39
38
  alias_method :h, :escape
40
39
 
@@ -43,12 +42,16 @@ module Liquid
43
42
  end
44
43
 
45
44
  def url_encode(input)
46
- CGI.escape(input) rescue input
45
+ CGI.escape(input) unless input.nil?
46
+ end
47
+
48
+ def url_decode(input)
49
+ CGI.unescape(input) unless input.nil?
47
50
  end
48
51
 
49
- def slice(input, offset, length=nil)
50
- offset = Integer(offset)
51
- length = length ? Integer(length) : 1
52
+ def slice(input, offset, length = nil)
53
+ offset = Utils.to_integer(offset)
54
+ length = length ? Utils.to_integer(length) : 1
52
55
 
53
56
  if input.is_a?(Array)
54
57
  input.slice(offset, length) || []
@@ -59,18 +62,22 @@ module Liquid
59
62
 
60
63
  # Truncate a string down to x characters
61
64
  def truncate(input, length = 50, truncate_string = "...".freeze)
62
- if input.nil? then return end
63
- l = length.to_i - truncate_string.length
65
+ return if input.nil?
66
+ input_str = input.to_s
67
+ length = Utils.to_integer(length)
68
+ truncate_string_str = truncate_string.to_s
69
+ l = length - truncate_string_str.length
64
70
  l = 0 if l < 0
65
- input.length > length.to_i ? input[0...l] + truncate_string : input
71
+ input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
66
72
  end
67
73
 
68
74
  def truncatewords(input, words = 15, truncate_string = "...".freeze)
69
- if input.nil? then return end
75
+ return if input.nil?
70
76
  wordlist = input.to_s.split
71
- l = words.to_i - 1
77
+ words = Utils.to_integer(words)
78
+ l = words - 1
72
79
  l = 0 if l < 0
73
- wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
80
+ wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input
74
81
  end
75
82
 
76
83
  # Split input string into an array of substrings separated by given pattern.
@@ -79,7 +86,7 @@ module Liquid
79
86
  # <div class="summary">{{ post | split '//' | first }}</div>
80
87
  #
81
88
  def split(input, pattern)
82
- input.to_s.split(pattern)
89
+ input.to_s.split(pattern.to_s)
83
90
  end
84
91
 
85
92
  def strip(input)
@@ -115,10 +122,32 @@ module Liquid
115
122
  ary = InputIterator.new(input)
116
123
  if property.nil?
117
124
  ary.sort
125
+ elsif ary.empty? # The next two cases assume a non-empty array.
126
+ []
118
127
  elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
119
- ary.sort {|a,b| a[property] <=> b[property] }
120
- elsif ary.first.respond_to?(property)
121
- ary.sort {|a,b| a.send(property) <=> b.send(property) }
128
+ ary.sort do |a, b|
129
+ a = a[property]
130
+ b = b[property]
131
+ if a && b
132
+ a <=> b
133
+ else
134
+ a ? -1 : 1
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ # Sort elements of an array ignoring case if strings
141
+ # provide optional property with which to sort an array of hashes or drops
142
+ def sort_natural(input, property = nil)
143
+ ary = InputIterator.new(input)
144
+
145
+ if property.nil?
146
+ ary.sort { |a, b| a.casecmp(b) }
147
+ elsif ary.empty? # The next two cases assume a non-empty array.
148
+ []
149
+ elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
150
+ ary.sort { |a, b| a[property].casecmp(b[property]) }
122
151
  end
123
152
  end
124
153
 
@@ -126,10 +155,13 @@ module Liquid
126
155
  # provide optional property with which to determine uniqueness
127
156
  def uniq(input, property = nil)
128
157
  ary = InputIterator.new(input)
158
+
129
159
  if property.nil?
130
- input.uniq
131
- elsif input.first.respond_to?(:[])
132
- input.uniq{ |a| a[property] }
160
+ ary.uniq
161
+ elsif ary.empty? # The next two cases assume a non-empty array.
162
+ []
163
+ elsif ary.first.respond_to?(:[])
164
+ ary.uniq{ |a| a[property] }
133
165
  end
134
166
  end
135
167
 
@@ -147,29 +179,44 @@ module Liquid
147
179
  if property == "to_liquid".freeze
148
180
  e
149
181
  elsif e.respond_to?(:[])
150
- e[property]
182
+ r = e[property]
183
+ r.is_a?(Proc) ? r.call : r
151
184
  end
152
185
  end
153
186
  end
154
187
 
188
+ # Remove nils within an array
189
+ # provide optional property with which to check for nil
190
+ def compact(input, property = nil)
191
+ ary = InputIterator.new(input)
192
+
193
+ if property.nil?
194
+ ary.compact
195
+ elsif ary.empty? # The next two cases assume a non-empty array.
196
+ []
197
+ elsif ary.first.respond_to?(:[])
198
+ ary.reject{ |a| a[property].nil? }
199
+ end
200
+ end
201
+
155
202
  # Replace occurrences of a string with another
156
203
  def replace(input, string, replacement = ''.freeze)
157
- input.to_s.gsub(string, replacement.to_s)
204
+ input.to_s.gsub(string.to_s, replacement.to_s)
158
205
  end
159
206
 
160
207
  # Replace the first occurrences of a string with another
161
208
  def replace_first(input, string, replacement = ''.freeze)
162
- input.to_s.sub(string, replacement.to_s)
209
+ input.to_s.sub(string.to_s, replacement.to_s)
163
210
  end
164
211
 
165
212
  # remove a substring
166
213
  def remove(input, string)
167
- input.to_s.gsub(string, ''.freeze)
214
+ input.to_s.gsub(string.to_s, ''.freeze)
168
215
  end
169
216
 
170
217
  # remove the first occurrences of a substring
171
218
  def remove_first(input, string)
172
- input.to_s.sub(string, ''.freeze)
219
+ input.to_s.sub(string.to_s, ''.freeze)
173
220
  end
174
221
 
175
222
  # add one string to another
@@ -177,6 +224,13 @@ module Liquid
177
224
  input.to_s + string.to_s
178
225
  end
179
226
 
227
+ def concat(input, array)
228
+ unless array.respond_to?(:to_ary)
229
+ raise ArgumentError.new("concat filter requires an array argument")
230
+ end
231
+ InputIterator.new(input).concat(array)
232
+ end
233
+
180
234
  # prepend a string to another
181
235
  def prepend(input, string)
182
236
  string.to_s + input.to_s
@@ -221,7 +275,7 @@ module Liquid
221
275
  def date(input, format)
222
276
  return input if format.to_s.empty?
223
277
 
224
- return input unless date = to_date(input)
278
+ return input unless date = Utils.to_date(input)
225
279
 
226
280
  date.strftime(format.to_s)
227
281
  end
@@ -244,6 +298,12 @@ module Liquid
244
298
  array.last if array.respond_to?(:last)
245
299
  end
246
300
 
301
+ # absolute value
302
+ def abs(input)
303
+ result = Utils.to_number(input).abs
304
+ result.is_a?(BigDecimal) ? result.to_f : result
305
+ end
306
+
247
307
  # addition
248
308
  def plus(input, operand)
249
309
  apply_operation(input, operand, :+)
@@ -262,66 +322,49 @@ module Liquid
262
322
  # division
263
323
  def divided_by(input, operand)
264
324
  apply_operation(input, operand, :/)
325
+ rescue ::ZeroDivisionError => e
326
+ raise Liquid::ZeroDivisionError, e.message
265
327
  end
266
328
 
267
329
  def modulo(input, operand)
268
330
  apply_operation(input, operand, :%)
331
+ rescue ::ZeroDivisionError => e
332
+ raise Liquid::ZeroDivisionError, e.message
269
333
  end
270
334
 
271
335
  def round(input, n = 0)
272
- result = to_number(input).round(to_number(n))
336
+ result = Utils.to_number(input).round(Utils.to_number(n))
273
337
  result = result.to_f if result.is_a?(BigDecimal)
274
338
  result = result.to_i if n == 0
275
339
  result
340
+ rescue ::FloatDomainError => e
341
+ raise Liquid::FloatDomainError, e.message
276
342
  end
277
343
 
278
344
  def ceil(input)
279
- to_number(input).ceil.to_i
345
+ Utils.to_number(input).ceil.to_i
346
+ rescue ::FloatDomainError => e
347
+ raise Liquid::FloatDomainError, e.message
280
348
  end
281
349
 
282
350
  def floor(input)
283
- to_number(input).floor.to_i
284
- end
285
-
286
- def default(input, default_value = "".freeze)
287
- is_blank = input.respond_to?(:empty?) ? input.empty? : !input
288
- is_blank ? default_value : input
351
+ Utils.to_number(input).floor.to_i
352
+ rescue ::FloatDomainError => e
353
+ raise Liquid::FloatDomainError, e.message
289
354
  end
290
355
 
291
- private
292
-
293
- def to_number(obj)
294
- case obj
295
- when Float
296
- BigDecimal.new(obj.to_s)
297
- when Numeric
298
- obj
299
- when String
300
- (obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
356
+ def default(input, default_value = ''.freeze)
357
+ if !input || input.respond_to?(:empty?) && input.empty?
358
+ default_value
301
359
  else
302
- 0
360
+ input
303
361
  end
304
362
  end
305
363
 
306
- def to_date(obj)
307
- return obj if obj.respond_to?(:strftime)
308
-
309
- case obj
310
- when 'now'.freeze, 'today'.freeze
311
- Time.now
312
- when /\A\d+\z/, Integer
313
- Time.at(obj.to_i)
314
- when String
315
- Time.parse(obj)
316
- else
317
- nil
318
- end
319
- rescue ::ArgumentError
320
- nil
321
- end
364
+ private
322
365
 
323
366
  def apply_operation(input, operand, operation)
324
- result = to_number(input).send(operation, to_number(operand))
367
+ result = Utils.to_number(input).send(operation, Utils.to_number(operand))
325
368
  result.is_a?(BigDecimal) ? result.to_f : result
326
369
  end
327
370
 
@@ -344,10 +387,27 @@ module Liquid
344
387
  to_a.join(glue)
345
388
  end
346
389
 
390
+ def concat(args)
391
+ to_a.concat(args)
392
+ end
393
+
347
394
  def reverse
348
395
  reverse_each.to_a
349
396
  end
350
397
 
398
+ def uniq(&block)
399
+ to_a.uniq(&block)
400
+ end
401
+
402
+ def compact
403
+ to_a.compact
404
+ end
405
+
406
+ def empty?
407
+ @input.each { return false }
408
+ true
409
+ end
410
+
351
411
  def each
352
412
  @input.each do |e|
353
413
  yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)