liquid 2.5.5 → 2.6.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +13 -5
  2. data/History.md +26 -11
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +6 -0
  5. data/lib/extras/liquid_view.rb +9 -9
  6. data/lib/liquid.rb +1 -0
  7. data/lib/liquid/block.rb +16 -5
  8. data/lib/liquid/context.rb +17 -9
  9. data/lib/liquid/document.rb +1 -1
  10. data/lib/liquid/errors.rb +2 -1
  11. data/lib/liquid/file_system.rb +12 -12
  12. data/lib/liquid/htmltags.rb +1 -1
  13. data/lib/liquid/module_ex.rb +1 -1
  14. data/lib/liquid/standardfilters.rb +44 -22
  15. data/lib/liquid/strainer.rb +3 -3
  16. data/lib/liquid/tag.rb +1 -1
  17. data/lib/liquid/tags/assign.rb +14 -12
  18. data/lib/liquid/tags/break.rb +2 -2
  19. data/lib/liquid/tags/capture.rb +1 -0
  20. data/lib/liquid/tags/case.rb +28 -28
  21. data/lib/liquid/tags/continue.rb +1 -1
  22. data/lib/liquid/tags/cycle.rb +21 -21
  23. data/lib/liquid/tags/decrement.rb +7 -7
  24. data/lib/liquid/tags/for.rb +26 -26
  25. data/lib/liquid/tags/if.rb +3 -3
  26. data/lib/liquid/tags/ifchanged.rb +9 -9
  27. data/lib/liquid/tags/include.rb +42 -14
  28. data/lib/liquid/tags/increment.rb +7 -7
  29. data/lib/liquid/tags/raw.rb +5 -4
  30. data/lib/liquid/tags/unless.rb +8 -8
  31. data/lib/liquid/template.rb +11 -5
  32. data/lib/liquid/utils.rb +0 -1
  33. data/lib/liquid/variable.rb +2 -2
  34. data/lib/liquid/version.rb +4 -0
  35. data/test/liquid/assign_test.rb +1 -1
  36. data/test/liquid/condition_test.rb +1 -1
  37. data/test/liquid/hash_ordering_test.rb +25 -0
  38. data/test/liquid/output_test.rb +2 -2
  39. data/test/liquid/standard_filter_test.rb +18 -0
  40. data/test/liquid/tags/for_tag_test.rb +47 -34
  41. data/test/liquid/tags/html_tag_test.rb +2 -2
  42. data/test/liquid/tags/if_else_tag_test.rb +0 -6
  43. data/test/liquid/tags/include_tag_test.rb +28 -1
  44. data/test/liquid/tags/raw_tag_test.rb +11 -2
  45. data/test/liquid/template_test.rb +72 -0
  46. data/test/liquid/variable_test.rb +6 -0
  47. metadata +8 -5
checksums.yaml CHANGED
@@ -1,7 +1,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 187e926592d08a1fdfe6c92a4438d2a28e3bef15
4
- data.tar.gz: da01b50919773dac4c35363aecc4d10c991c4ed7
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZTIwZjU2MjYxMGExMjE4NWMxM2YwMzdlOGY4ZThmNDFmZGMzZGYzMA==
5
+ data.tar.gz: !binary |-
6
+ NTgxNTU0ODAxMGEyZGNlN2VkMDgxNDAxZGZkZmNiZjJjZjdhZmJiNA==
5
7
  SHA512:
6
- metadata.gz: 0f676eb449b0af41596e80b8f9bbdc76ae101339a2a4cf97d65b1a75a1d9a1bd3ccdeb9787d53f61080d5f7c1c9456e3cec550e5c0d8f4f848d9c27acd7e5d37
7
- data.tar.gz: c2c869dfd70ef5ecafb1c2095cc513e7b38d1a023bca6fb6ebfebee1374e07c6a3043f52396a55601c60dd71abe90ca6f71c0ff112d250285aaf70d9fe738023
8
+ metadata.gz: !binary |-
9
+ NjQ4YTI3NWZjYmE2ZTZiYzkzOTI1NDE0NTQ3M2EwN2Q4NGNiYjUxM2NjZmEy
10
+ NWNhMmVlNzY1YTM4NWU1NzNjYjAzMzI2NzhlOWYyMTkxYTA5NTkyYjBiMjJj
11
+ NzdkMDAzNzRjNmIwODk1MGJjNjNjZjY2MTg3MmVjNGM4NjhkMTU=
12
+ data.tar.gz: !binary |-
13
+ MDQ1ZWE4MDM0NTU5MmFlOWU3ZmQzMWE2NTEyOTMzYjVjODViZDJkNGM3ODQ1
14
+ NmYyNDM1M2NiYjAzZmFhMThmNDIyOGJmN2E5NmFmODQ4YWY3MTY3MjZlMTg0
15
+ Y2I1ODVlMmJkZDQyYTMwOTY1MWM5ZDkyOTQ0NTA1MmE0NTEzYWQ=
data/History.md CHANGED
@@ -1,16 +1,30 @@
1
1
  # Liquid Version History
2
2
 
3
- ## 2.5.5 / 2014-01-10 / branch "2-5-stable"
4
-
5
- Security fix, cherry-picked from master (4e14a65):
6
- * Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk]
7
- * Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith]
8
-
9
- ## 2.5.4 / 2013-11-11
10
-
11
- * Fix "can't convert Fixnum into String" for "replace", see #173, [wǒ_is神仙, jsw0528]
12
-
13
- ## 2.5.3 / 2013-10-09
3
+ IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains explicit Ruby 1.8 compatability.
4
+ The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8.
5
+
6
+ ## 2.6.0 (not yet released)
7
+
8
+ * ...
9
+ * Bugfix for #106: fix example servlet [gnowoel]
10
+ * Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss, joliss]
11
+ * Bugfix for #114: strip_html filter supports style tags [James Allardice, jamesallardice]
12
+ * Bugfix for #117: 'now' support for date filter in Ruby 1.9 [Notre Dame Webgroup, ndwebgroup]
13
+ * Bugfix for #166: truncate filter on UTF-8 strings with Ruby 1.8 [Florian Weingarten, fw42]
14
+ * Bugfix for #204: 'raw' parsing bug [Florian Weingarten, fw42]
15
+ * Bugfix for #150: 'for' parsing bug [Peter Schröder, phoet]
16
+ * Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder, phoet]
17
+ * Allow a Liquid::Drop to be passed into Template#render [Daniel Huckstep, darkhelmet]
18
+ * Resource limits [Florian Weingarten, fw42]
19
+ * Add reverse filter [Jay Strybis, unreal]
20
+ * Add utf-8 support
21
+ * Use array instead of Hash to keep the registered filters [Tasos Stathopoulos, astathopoulos]
22
+ * Cache tokenized partial templates [Tom Burns, boourns]
23
+ * Avoid warnings in Ruby 1.9.3 [Marcus Stollsteimer, stomar]
24
+ * Better documentation for 'include' tag (closes #163) [Peter Schröder, phoet]
25
+ * Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves, arthurnn]
26
+
27
+ ## 2.5.3 / branch "2.5-stable"
14
28
 
15
29
  * #232, #234, #237: Fix map filter bugs [Florian Weingarten, fw42]
16
30
 
@@ -30,6 +44,7 @@ Yanked from rubygems, as it contained too many changes that broke compatibility.
30
44
  * Fix filter parser for args without space separators
31
45
  * Add support for filter keyword arguments
32
46
 
47
+
33
48
  ## 2.4.0 / 2012-08-03
34
49
 
35
50
  * Performance improvements
data/MIT-LICENSE CHANGED
@@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
17
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
18
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
19
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Liquid template engine
2
2
 
3
+ * [Contributing guidelines](CONTRIBUTING.md)
4
+ * [Version history](History.md)
5
+ * [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
6
+ * [Liquid Wiki from Shopify](http://wiki.shopify.com/Liquid)
7
+ * [Website](http://liquidmarkup.org/)
8
+
3
9
  ## Introduction
4
10
 
5
11
  Liquid is a template engine which was written with very specific requirements:
@@ -2,15 +2,15 @@
2
2
  # and use liquid as an template system for .liquid files
3
3
  #
4
4
  # Example
5
- #
5
+ #
6
6
  # ActionView::Base::register_template_handler :liquid, LiquidView
7
7
  class LiquidView
8
8
  PROTECTED_ASSIGNS = %w( template_root response _session template_class action_name request_origin session template
9
9
  _response url _request _cookies variables_added _flash params _headers request cookies
10
10
  ignore_missing_templates flash _params logger before_filter_chain_aborted headers )
11
- PROTECTED_INSTANCE_VARIABLES = %w( @_request @controller @_first_render @_memoized__pick_template @view_paths
11
+ PROTECTED_INSTANCE_VARIABLES = %w( @_request @controller @_first_render @_memoized__pick_template @view_paths
12
12
  @helpers @assigns_added @template @_render_stack @template_format @assigns )
13
-
13
+
14
14
  def self.call(template)
15
15
  "LiquidView.new(self).render(template, local_assigns)"
16
16
  end
@@ -18,10 +18,10 @@ class LiquidView
18
18
  def initialize(view)
19
19
  @view = view
20
20
  end
21
-
21
+
22
22
  def render(template, local_assigns = nil)
23
23
  @view.controller.headers["Content-Type"] ||= 'text/html; charset=utf-8'
24
-
24
+
25
25
  # Rails 2.2 Template has source, but not locals
26
26
  if template.respond_to?(:source) && !template.respond_to?(:locals)
27
27
  assigns = (@view.instance_variables - PROTECTED_INSTANCE_VARIABLES).inject({}) do |hash, ivar|
@@ -31,15 +31,15 @@ class LiquidView
31
31
  else
32
32
  assigns = @view.assigns.reject{ |k,v| PROTECTED_ASSIGNS.include?(k) }
33
33
  end
34
-
34
+
35
35
  source = template.respond_to?(:source) ? template.source : template
36
36
  local_assigns = (template.respond_to?(:locals) ? template.locals : local_assigns) || {}
37
-
37
+
38
38
  if content_for_layout = @view.instance_variable_get("@content_for_layout")
39
39
  assigns['content_for_layout'] = content_for_layout
40
40
  end
41
41
  assigns.merge!(local_assigns.stringify_keys)
42
-
42
+
43
43
  liquid = Liquid::Template.parse(source)
44
44
  liquid.render(assigns, :filters => [@view.controller.master_helper_module], :registers => {:action_view => @view, :controller => @view.controller})
45
45
  end
@@ -48,4 +48,4 @@ class LiquidView
48
48
  false
49
49
  end
50
50
 
51
- end
51
+ end
data/lib/liquid.rb CHANGED
@@ -45,6 +45,7 @@ module Liquid
45
45
  VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
46
46
  end
47
47
 
48
+ require "liquid/version"
48
49
  require 'liquid/drop'
49
50
  require 'liquid/extensions'
50
51
  require 'liquid/errors'
data/lib/liquid/block.rb CHANGED
@@ -16,7 +16,7 @@ module Liquid
16
16
  when IsTag
17
17
  if token =~ FullToken
18
18
 
19
- # if we found the proper block delimitor just end parsing here and let the outer block
19
+ # if we found the proper block delimiter just end parsing here and let the outer block
20
20
  # proceed
21
21
  if block_delimiter == $1
22
22
  end_tag
@@ -43,8 +43,8 @@ module Liquid
43
43
  end
44
44
  end
45
45
 
46
- # Make sure that its ok to end parsing in the current block.
47
- # Effectively this method will throw and exception unless the current block is
46
+ # Make sure that it's ok to end parsing in the current block.
47
+ # Effectively this method will throw an exception unless the current block is
48
48
  # of type Document
49
49
  assert_missing_delimitation!
50
50
  end
@@ -90,20 +90,31 @@ module Liquid
90
90
 
91
91
  def render_all(list, context)
92
92
  output = []
93
+ context.resource_limits[:render_length_current] = 0
94
+ context.resource_limits[:render_score_current] += list.length
95
+
93
96
  list.each do |token|
94
97
  # Break out if we have any unhanded interrupts.
95
98
  break if context.has_interrupt?
96
99
 
97
100
  begin
98
101
  # If we get an Interrupt that means the block must stop processing. An
99
- # Interrupt is any command that stops block execution such as {% break %}
102
+ # Interrupt is any command that stops block execution such as {% break %}
100
103
  # or {% continue %}
101
104
  if token.is_a? Continue or token.is_a? Break
102
105
  context.push_interrupt(token.interrupt)
103
106
  break
104
107
  end
105
108
 
106
- output << (token.respond_to?(:render) ? token.render(context) : token)
109
+ token_output = (token.respond_to?(:render) ? token.render(context) : token)
110
+ context.resource_limits[:render_length_current] += (token_output.respond_to?(:length) ? token_output.length : 1)
111
+ if context.resource_limits_reached?
112
+ context.resource_limits[:reached] = true
113
+ raise MemoryError.new("Memory limits exceeded")
114
+ end
115
+ output << token_output
116
+ rescue MemoryError => e
117
+ raise e
107
118
  rescue ::StandardError => e
108
119
  output << (context.handle_error(e))
109
120
  end
@@ -13,19 +13,26 @@ module Liquid
13
13
  #
14
14
  # context['bob'] #=> nil class Context
15
15
  class Context
16
- attr_reader :scopes, :errors, :registers, :environments
17
-
18
- def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false)
19
- @environments = [environments].flatten
20
- @scopes = [(outer_scope || {})]
21
- @registers = registers
22
- @errors = []
23
- @rethrow_errors = rethrow_errors
16
+ attr_reader :scopes, :errors, :registers, :environments, :resource_limits
17
+
18
+ def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
19
+ @environments = [environments].flatten
20
+ @scopes = [(outer_scope || {})]
21
+ @registers = registers
22
+ @errors = []
23
+ @rethrow_errors = rethrow_errors
24
+ @resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
24
25
  squash_instance_assigns_with_environments
25
26
 
26
27
  @interrupts = []
27
28
  end
28
29
 
30
+ def resource_limits_reached?
31
+ (@resource_limits[:render_length_limit] && @resource_limits[:render_length_current] > @resource_limits[:render_length_limit]) ||
32
+ (@resource_limits[:render_score_limit] && @resource_limits[:render_score_current] > @resource_limits[:render_score_limit] ) ||
33
+ (@resource_limits[:assign_score_limit] && @resource_limits[:assign_score_current] > @resource_limits[:assign_score_limit] )
34
+ end
35
+
29
36
  def strainer
30
37
  @strainer ||= Strainer.create(self)
31
38
  end
@@ -46,7 +53,7 @@ module Liquid
46
53
 
47
54
  # are there any not handled interrupts?
48
55
  def has_interrupt?
49
- !@interrupts.empty?
56
+ @interrupts.any?
50
57
  end
51
58
 
52
59
  # push an interrupt to the stack. this interrupt is considered not handled.
@@ -165,6 +172,7 @@ module Liquid
165
172
  # Fetches an object starting at the local scope and then moving up the hierachy
166
173
  def find_variable(key)
167
174
  scope = @scopes.find { |s| s.has_key?(key) }
175
+ variable = nil
168
176
 
169
177
  if scope.nil?
170
178
  @environments.each do |e|
@@ -5,7 +5,7 @@ module Liquid
5
5
  parse(tokens)
6
6
  end
7
7
 
8
- # There isn't a real delimter
8
+ # There isn't a real delimiter
9
9
  def block_delimiter
10
10
  []
11
11
  end
data/lib/liquid/errors.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Liquid
2
2
  class Error < ::StandardError; end
3
-
3
+
4
4
  class ArgumentError < Error; end
5
5
  class ContextError < Error; end
6
6
  class FilterNotFound < Error; end
@@ -8,4 +8,5 @@ module Liquid
8
8
  class StandardError < Error; end
9
9
  class SyntaxError < Error; end
10
10
  class StackLevelError < Error; end
11
+ class MemoryError < Error; end
11
12
  end
@@ -1,7 +1,7 @@
1
1
  module Liquid
2
- # A Liquid file system is way to let your templates retrieve other templates for use with the include tag.
2
+ # A Liquid file system is a way to let your templates retrieve other templates for use with the include tag.
3
3
  #
4
- # You can implement subclasses that retrieve templates from the database, from the file system using a different
4
+ # You can implement subclasses that retrieve templates from the database, from the file system using a different
5
5
  # path structure, you can provide them as hard-coded inline strings, or any manner that you see fit.
6
6
  #
7
7
  # You can add additional instance variables, arguments, or methods as needed.
@@ -18,7 +18,7 @@ module Liquid
18
18
  raise FileSystemError, "This liquid context does not allow includes."
19
19
  end
20
20
  end
21
-
21
+
22
22
  # This implements an abstract file system which retrieves template files named in a manner similar to Rails partials,
23
23
  # ie. with the template name prefixed with an underscore. The extension ".liquid" is also added.
24
24
  #
@@ -27,36 +27,36 @@ module Liquid
27
27
  # Example:
28
28
  #
29
29
  # file_system = Liquid::LocalFileSystem.new("/some/path")
30
- #
30
+ #
31
31
  # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
32
32
  # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
33
33
  #
34
34
  class LocalFileSystem
35
35
  attr_accessor :root
36
-
36
+
37
37
  def initialize(root)
38
38
  @root = root
39
39
  end
40
-
40
+
41
41
  def read_template_file(template_path, context)
42
42
  full_path = full_path(template_path)
43
43
  raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path)
44
-
44
+
45
45
  File.read(full_path)
46
46
  end
47
-
47
+
48
48
  def full_path(template_path)
49
49
  raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/
50
-
50
+
51
51
  full_path = if template_path.include?('/')
52
52
  File.join(root, File.dirname(template_path), "_#{File.basename(template_path)}.liquid")
53
53
  else
54
54
  File.join(root, "_#{template_path}.liquid")
55
55
  end
56
-
56
+
57
57
  raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(root)}/
58
-
58
+
59
59
  full_path
60
60
  end
61
61
  end
62
- end
62
+ end
@@ -57,7 +57,7 @@ module Liquid
57
57
 
58
58
  result << "<td class=\"col#{col}\">" << render_all(@nodelist, context) << '</td>'
59
59
 
60
- if col == cols and not (index == length - 1)
60
+ if col == cols and (index != length - 1)
61
61
  col = 0
62
62
  row += 1
63
63
  result << "</tr>\n<tr class=\"row#{row}\">"
@@ -2,7 +2,7 @@
2
2
  # This library is free software. It may be used, redistributed and/or modified
3
3
  # under the same terms as Ruby itself
4
4
  #
5
- # This extension is usesd in order to expose the object of the implementing class
5
+ # This extension is used in order to expose the object of the implementing class
6
6
  # to liquid as it were a Drop. It also limits the liquid-callable methods of the instance
7
7
  # to the allowed method passed with the liquid_methods call
8
8
  # Example:
@@ -1,4 +1,5 @@
1
1
  require 'cgi'
2
+ require 'bigdecimal'
2
3
 
3
4
  module Liquid
4
5
 
@@ -10,12 +11,12 @@ module Liquid
10
11
  input.respond_to?(:size) ? input.size : 0
11
12
  end
12
13
 
13
- # convert a input string to DOWNCASE
14
+ # convert an input string to DOWNCASE
14
15
  def downcase(input)
15
16
  input.to_s.downcase
16
17
  end
17
18
 
18
- # convert a input string to UPCASE
19
+ # convert an input string to UPCASE
19
20
  def upcase(input)
20
21
  input.to_s.upcase
21
22
  end
@@ -42,7 +43,8 @@ module Liquid
42
43
  if input.nil? then return end
43
44
  l = length.to_i - truncate_string.length
44
45
  l = 0 if l < 0
45
- input.length > length.to_i ? input[0...l] + truncate_string : input
46
+ truncated = RUBY_VERSION[0,3] == "1.8" ? input.scan(/./mu)[0...l].to_s : input[0...l]
47
+ input.length > length.to_i ? truncated + truncate_string : input
46
48
  end
47
49
 
48
50
  def truncatewords(input, words = 15, truncate_string = "...")
@@ -63,15 +65,14 @@ module Liquid
63
65
  end
64
66
 
65
67
  def strip_html(input)
66
- input.to_s.gsub(/<script.*?<\/script>/, '').gsub(/<!--.*?-->/, '').gsub(/<.*?>/, '')
68
+ input.to_s.gsub(/<script.*?<\/script>/m, '').gsub(/<!--.*?-->/m, '').gsub(/<style.*?<\/style>/m, '').gsub(/<.*?>/m, '')
67
69
  end
68
70
 
69
71
  # Remove all newlines from the string
70
72
  def strip_newlines(input)
71
- input.to_s.gsub(/\n/, '')
73
+ input.to_s.gsub(/\r?\n/, '')
72
74
  end
73
75
 
74
-
75
76
  # Join elements of the array with certain character between them
76
77
  def join(input, glue = ' ')
77
78
  [input].flatten.join(glue)
@@ -90,6 +91,12 @@ module Liquid
90
91
  end
91
92
  end
92
93
 
94
+ # Reverse the elements of an array
95
+ def reverse(input)
96
+ ary = [input].flatten
97
+ ary.reverse
98
+ end
99
+
93
100
  # map/collect on a given property
94
101
  def map(input, property)
95
102
  ary = [input].flatten
@@ -178,14 +185,23 @@ module Liquid
178
185
  input = Time.at(input.to_i)
179
186
  end
180
187
 
181
- date = input.is_a?(String) ? Time.parse(input) : input
188
+ date = if input.is_a?(String)
189
+ case input.downcase
190
+ when 'now', 'today'
191
+ Time.now
192
+ else
193
+ Time.parse(input)
194
+ end
195
+ else
196
+ input
197
+ end
182
198
 
183
199
  if date.respond_to?(:strftime)
184
200
  date.strftime(format.to_s)
185
201
  else
186
202
  input
187
203
  end
188
- rescue => e
204
+ rescue
189
205
  input
190
206
  end
191
207
 
@@ -209,41 +225,47 @@ module Liquid
209
225
 
210
226
  # addition
211
227
  def plus(input, operand)
212
- to_number(input) + to_number(operand)
228
+ apply_operation(input, operand, :+)
213
229
  end
214
230
 
215
231
  # subtraction
216
232
  def minus(input, operand)
217
- to_number(input) - to_number(operand)
233
+ apply_operation(input, operand, :-)
218
234
  end
219
235
 
220
236
  # multiplication
221
237
  def times(input, operand)
222
- to_number(input) * to_number(operand)
238
+ apply_operation(input, operand, :*)
223
239
  end
224
240
 
225
241
  # division
226
242
  def divided_by(input, operand)
227
- to_number(input) / to_number(operand)
243
+ apply_operation(input, operand, :/)
228
244
  end
229
245
 
230
246
  def modulo(input, operand)
231
- to_number(input) % to_number(operand)
247
+ apply_operation(input, operand, :%)
232
248
  end
233
249
 
234
250
  private
235
251
 
236
- def to_number(obj)
237
- case obj
238
- when Numeric
239
- obj
240
- when String
241
- (obj.strip =~ /^\d+\.\d+$/) ? obj.to_f : obj.to_i
242
- else
243
- 0
244
- end
252
+ def to_number(obj)
253
+ case obj
254
+ when Float
255
+ BigDecimal.new(obj.to_s)
256
+ when Numeric
257
+ obj
258
+ when String
259
+ (obj.strip =~ /^\d+\.\d+$/) ? BigDecimal.new(obj) : obj.to_i
260
+ else
261
+ 0
245
262
  end
263
+ end
246
264
 
265
+ def apply_operation(input, operand, operation)
266
+ result = to_number(input).send(operation, to_number(operand))
267
+ result.is_a?(BigDecimal) ? result.to_f : result
268
+ end
247
269
  end
248
270
 
249
271
  Template.register_filter(StandardFilters)