liquid 3.0.6 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
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)