liquid 2.6.3 → 3.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +46 -13
  3. data/README.md +27 -2
  4. data/lib/liquid/block.rb +85 -51
  5. data/lib/liquid/block_body.rb +123 -0
  6. data/lib/liquid/condition.rb +26 -15
  7. data/lib/liquid/context.rb +106 -140
  8. data/lib/liquid/document.rb +3 -3
  9. data/lib/liquid/drop.rb +17 -1
  10. data/lib/liquid/errors.rb +50 -2
  11. data/lib/liquid/expression.rb +33 -0
  12. data/lib/liquid/file_system.rb +17 -6
  13. data/lib/liquid/i18n.rb +39 -0
  14. data/lib/liquid/interrupts.rb +1 -1
  15. data/lib/liquid/lexer.rb +51 -0
  16. data/lib/liquid/locales/en.yml +22 -0
  17. data/lib/liquid/parser.rb +90 -0
  18. data/lib/liquid/parser_switching.rb +31 -0
  19. data/lib/liquid/profiler/hooks.rb +23 -0
  20. data/lib/liquid/profiler.rb +159 -0
  21. data/lib/liquid/range_lookup.rb +22 -0
  22. data/lib/liquid/standardfilters.rb +143 -55
  23. data/lib/liquid/strainer.rb +14 -4
  24. data/lib/liquid/tag.rb +25 -9
  25. data/lib/liquid/tags/assign.rb +12 -9
  26. data/lib/liquid/tags/break.rb +1 -1
  27. data/lib/liquid/tags/capture.rb +10 -8
  28. data/lib/liquid/tags/case.rb +13 -13
  29. data/lib/liquid/tags/comment.rb +9 -2
  30. data/lib/liquid/tags/continue.rb +1 -4
  31. data/lib/liquid/tags/cycle.rb +5 -7
  32. data/lib/liquid/tags/decrement.rb +3 -4
  33. data/lib/liquid/tags/for.rb +69 -36
  34. data/lib/liquid/tags/if.rb +52 -25
  35. data/lib/liquid/tags/ifchanged.rb +3 -3
  36. data/lib/liquid/tags/include.rb +19 -8
  37. data/lib/liquid/tags/increment.rb +4 -8
  38. data/lib/liquid/tags/raw.rb +4 -7
  39. data/lib/liquid/tags/table_row.rb +73 -0
  40. data/lib/liquid/tags/unless.rb +2 -4
  41. data/lib/liquid/template.rb +124 -14
  42. data/lib/liquid/token.rb +18 -0
  43. data/lib/liquid/utils.rb +13 -4
  44. data/lib/liquid/variable.rb +103 -25
  45. data/lib/liquid/variable_lookup.rb +78 -0
  46. data/lib/liquid/version.rb +1 -1
  47. data/lib/liquid.rb +19 -11
  48. data/test/fixtures/en_locale.yml +9 -0
  49. data/test/{liquid → integration}/assign_test.rb +18 -1
  50. data/test/integration/blank_test.rb +106 -0
  51. data/test/{liquid → integration}/capture_test.rb +3 -3
  52. data/test/integration/context_test.rb +32 -0
  53. data/test/integration/drop_test.rb +271 -0
  54. data/test/integration/error_handling_test.rb +207 -0
  55. data/test/{liquid → integration}/filter_test.rb +11 -11
  56. data/test/integration/hash_ordering_test.rb +23 -0
  57. data/test/{liquid → integration}/output_test.rb +13 -13
  58. data/test/integration/parsing_quirks_test.rb +116 -0
  59. data/test/integration/render_profiling_test.rb +154 -0
  60. data/test/{liquid → integration}/security_test.rb +10 -10
  61. data/test/{liquid → integration}/standard_filter_test.rb +148 -32
  62. data/test/{liquid → integration}/tags/break_tag_test.rb +1 -1
  63. data/test/{liquid → integration}/tags/continue_tag_test.rb +1 -1
  64. data/test/{liquid → integration}/tags/for_tag_test.rb +80 -2
  65. data/test/{liquid → integration}/tags/if_else_tag_test.rb +24 -21
  66. data/test/integration/tags/include_tag_test.rb +234 -0
  67. data/test/{liquid → integration}/tags/increment_tag_test.rb +1 -1
  68. data/test/{liquid → integration}/tags/raw_tag_test.rb +2 -1
  69. data/test/{liquid → integration}/tags/standard_tag_test.rb +28 -26
  70. data/test/integration/tags/statements_test.rb +113 -0
  71. data/test/{liquid/tags/html_tag_test.rb → integration/tags/table_row_test.rb} +5 -5
  72. data/test/{liquid → integration}/tags/unless_else_tag_test.rb +1 -1
  73. data/test/{liquid → integration}/template_test.rb +81 -45
  74. data/test/integration/variable_test.rb +82 -0
  75. data/test/test_helper.rb +73 -20
  76. data/test/{liquid/block_test.rb → unit/block_unit_test.rb} +2 -5
  77. data/test/{liquid/condition_test.rb → unit/condition_unit_test.rb} +23 -1
  78. data/test/{liquid/context_test.rb → unit/context_unit_test.rb} +39 -25
  79. data/test/{liquid/file_system_test.rb → unit/file_system_unit_test.rb} +11 -5
  80. data/test/unit/i18n_unit_test.rb +37 -0
  81. data/test/unit/lexer_unit_test.rb +48 -0
  82. data/test/{liquid/module_ex_test.rb → unit/module_ex_unit_test.rb} +7 -7
  83. data/test/unit/parser_unit_test.rb +82 -0
  84. data/test/{liquid/regexp_test.rb → unit/regexp_unit_test.rb} +3 -3
  85. data/test/{liquid/strainer_test.rb → unit/strainer_unit_test.rb} +20 -1
  86. data/test/unit/tag_unit_test.rb +16 -0
  87. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  88. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  89. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  90. data/test/unit/template_unit_test.rb +69 -0
  91. data/test/unit/tokenizer_unit_test.rb +38 -0
  92. data/test/unit/variable_unit_test.rb +139 -0
  93. metadata +135 -67
  94. data/lib/extras/liquid_view.rb +0 -51
  95. data/lib/liquid/htmltags.rb +0 -73
  96. data/test/liquid/drop_test.rb +0 -180
  97. data/test/liquid/error_handling_test.rb +0 -81
  98. data/test/liquid/hash_ordering_test.rb +0 -25
  99. data/test/liquid/parsing_quirks_test.rb +0 -52
  100. data/test/liquid/tags/include_tag_test.rb +0 -166
  101. data/test/liquid/tags/statements_test.rb +0 -134
  102. data/test/liquid/variable_test.rb +0 -186
@@ -0,0 +1,90 @@
1
+ module Liquid
2
+ class Parser
3
+ def initialize(input)
4
+ l = Lexer.new(input)
5
+ @tokens = l.tokenize
6
+ @p = 0 # pointer to current location
7
+ end
8
+
9
+ def jump(point)
10
+ @p = point
11
+ end
12
+
13
+ def consume(type = nil)
14
+ token = @tokens[@p]
15
+ if type && token[0] != type
16
+ raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}"
17
+ end
18
+ @p += 1
19
+ token[1]
20
+ end
21
+
22
+ # Only consumes the token if it matches the type
23
+ # Returns the token's contents if it was consumed
24
+ # or false otherwise.
25
+ def consume?(type)
26
+ token = @tokens[@p]
27
+ return false unless token && token[0] == type
28
+ @p += 1
29
+ token[1]
30
+ end
31
+
32
+ # Like consume? Except for an :id token of a certain name
33
+ def id?(str)
34
+ token = @tokens[@p]
35
+ return false unless token && token[0] == :id
36
+ return false unless token[1] == str
37
+ @p += 1
38
+ token[1]
39
+ end
40
+
41
+ def look(type, ahead = 0)
42
+ tok = @tokens[@p + ahead]
43
+ return false unless tok
44
+ tok[0] == type
45
+ end
46
+
47
+ def expression
48
+ token = @tokens[@p]
49
+ if token[0] == :id
50
+ variable_signature
51
+ elsif [:string, :number].include? token[0]
52
+ consume
53
+ elsif token.first == :open_round
54
+ consume
55
+ first = expression
56
+ consume(:dotdot)
57
+ last = expression
58
+ consume(:close_round)
59
+ "(#{first}..#{last})"
60
+ else
61
+ raise SyntaxError, "#{token} is not a valid expression"
62
+ end
63
+ end
64
+
65
+ def argument
66
+ str = ""
67
+ # might be a keyword argument (identifier: expression)
68
+ if look(:id) && look(:colon, 1)
69
+ str << consume << consume << ' '.freeze
70
+ end
71
+
72
+ str << expression
73
+ str
74
+ end
75
+
76
+ def variable_signature
77
+ str = consume(:id)
78
+ if look(:open_square)
79
+ str << consume
80
+ str << expression
81
+ str << consume(:close_square)
82
+ end
83
+ if look(:dot)
84
+ str << consume
85
+ str << variable_signature
86
+ end
87
+ str
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,31 @@
1
+ module Liquid
2
+ module ParserSwitching
3
+ def parse_with_selected_parser(markup)
4
+ case @options[:error_mode] || Template.error_mode
5
+ when :strict then strict_parse_with_error_context(markup)
6
+ when :lax then lax_parse(markup)
7
+ when :warn
8
+ begin
9
+ return strict_parse_with_error_context(markup)
10
+ rescue SyntaxError => e
11
+ e.set_line_number_from_token(markup)
12
+ @warnings ||= []
13
+ @warnings << e
14
+ return lax_parse(markup)
15
+ end
16
+ end
17
+ end
18
+
19
+ private
20
+ def strict_parse_with_error_context(markup)
21
+ strict_parse(markup)
22
+ rescue SyntaxError => e
23
+ e.markup_context = markup_context(markup)
24
+ raise e
25
+ end
26
+
27
+ def markup_context(markup)
28
+ "in \"#{markup.strip}\""
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
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)
6
+ end
7
+ end
8
+
9
+ alias_method :render_token_without_profiling, :render_token
10
+ alias_method :render_token, :render_token_with_profiling
11
+ end
12
+
13
+ class Include < Tag
14
+ def render_with_profiling(context)
15
+ Profiler.profile_children(@template_name) do
16
+ render_without_profiling(context)
17
+ end
18
+ end
19
+
20
+ alias_method :render_without_profiling, :render
21
+ alias_method :render, :render_with_profiling
22
+ end
23
+ end
@@ -0,0 +1,159 @@
1
+ module Liquid
2
+
3
+ # Profiler enables support for profiling template rendering to help track down performance issues.
4
+ #
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
7
+ # class via the <tt>Liquid::Template#profiler</tt> method.
8
+ #
9
+ # template = Liquid::Template.parse(template_content, profile: true)
10
+ # output = template.render
11
+ # profile = template.profiler
12
+ #
13
+ # This object contains all profiling information, containing information on what tags were rendered,
14
+ # where in the templates these tags live, and how long each tag took to render.
15
+ #
16
+ # This is a tree structure that is Enumerable all the way down, and keeps track of tags and rendering times
17
+ # inside of <tt>{% include %}</tt> tags.
18
+ #
19
+ # profile.each do |node|
20
+ # # Access to the token itself
21
+ # node.code
22
+ #
23
+ # # Which template and line number of this node.
24
+ # # If top level, this will be "<root>".
25
+ # node.partial
26
+ # node.line_number
27
+ #
28
+ # # Render time in seconds of this node
29
+ # node.render_time
30
+ #
31
+ # # If the template used {% include %}, this node will also have children.
32
+ # node.children.each do |child2|
33
+ # # ...
34
+ # end
35
+ # end
36
+ #
37
+ # Profiler also exposes the total time of the template's render in <tt>Liquid::Profiler#total_render_time</tt>.
38
+ #
39
+ # All render times are in seconds. There is a small performance hit when profiling is enabled.
40
+ #
41
+ class Profiler
42
+ include Enumerable
43
+
44
+ class Timing
45
+ attr_reader :code, :partial, :line_number, :children
46
+
47
+ def initialize(token, partial)
48
+ @code = token.respond_to?(:raw) ? token.raw : token
49
+ @partial = partial
50
+ @line_number = token.respond_to?(:line_number) ? token.line_number : nil
51
+ @children = []
52
+ end
53
+
54
+ def self.start(token, partial)
55
+ new(token, partial).tap do |t|
56
+ t.start
57
+ end
58
+ end
59
+
60
+ def start
61
+ @start_time = Time.now
62
+ end
63
+
64
+ def finish
65
+ @end_time = Time.now
66
+ end
67
+
68
+ def render_time
69
+ @end_time - @start_time
70
+ end
71
+ end
72
+
73
+ def self.profile_token_render(token)
74
+ if Profiler.current_profile && token.respond_to?(:render)
75
+ Profiler.current_profile.start_token(token)
76
+ output = yield
77
+ Profiler.current_profile.end_token(token)
78
+ output
79
+ else
80
+ yield
81
+ end
82
+ end
83
+
84
+ def self.profile_children(template_name)
85
+ if Profiler.current_profile
86
+ Profiler.current_profile.push_partial(template_name)
87
+ output = yield
88
+ Profiler.current_profile.pop_partial
89
+ output
90
+ else
91
+ yield
92
+ end
93
+ end
94
+
95
+ def self.current_profile
96
+ Thread.current[:liquid_profiler]
97
+ end
98
+
99
+ def initialize
100
+ @partial_stack = ["<root>"]
101
+
102
+ @root_timing = Timing.new("", current_partial)
103
+ @timing_stack = [@root_timing]
104
+
105
+ @render_start_at = Time.now
106
+ @render_end_at = @render_start_at
107
+ end
108
+
109
+ def start
110
+ Thread.current[:liquid_profiler] = self
111
+ @render_start_at = Time.now
112
+ end
113
+
114
+ def stop
115
+ Thread.current[:liquid_profiler] = nil
116
+ @render_end_at = Time.now
117
+ end
118
+
119
+ def total_render_time
120
+ @render_end_at - @render_start_at
121
+ end
122
+
123
+ def each(&block)
124
+ @root_timing.children.each(&block)
125
+ end
126
+
127
+ def [](idx)
128
+ @root_timing.children[idx]
129
+ end
130
+
131
+ def length
132
+ @root_timing.children.length
133
+ end
134
+
135
+ def start_token(token)
136
+ @timing_stack.push(Timing.start(token, current_partial))
137
+ end
138
+
139
+ def end_token(token)
140
+ timing = @timing_stack.pop
141
+ timing.finish
142
+
143
+ @timing_stack.last.children << timing
144
+ end
145
+
146
+ def current_partial
147
+ @partial_stack.last
148
+ end
149
+
150
+ def push_partial(partial_name)
151
+ @partial_stack.push(partial_name)
152
+ end
153
+
154
+ def pop_partial
155
+ @partial_stack.pop
156
+ end
157
+
158
+ end
159
+ end
@@ -0,0 +1,22 @@
1
+ module Liquid
2
+ class RangeLookup
3
+ def self.parse(start_markup, end_markup)
4
+ start_obj = Expression.parse(start_markup)
5
+ end_obj = Expression.parse(end_markup)
6
+ if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
7
+ new(start_obj, end_obj)
8
+ else
9
+ start_obj.to_i..end_obj.to_i
10
+ end
11
+ end
12
+
13
+ def initialize(start_obj, end_obj)
14
+ @start_obj = start_obj
15
+ @end_obj = end_obj
16
+ end
17
+
18
+ def evaluate(context)
19
+ context.evaluate(@start_obj).to_i..context.evaluate(@end_obj).to_i
20
+ end
21
+ end
22
+ end
@@ -4,10 +4,17 @@ require 'bigdecimal'
4
4
  module Liquid
5
5
 
6
6
  module StandardFilters
7
+ HTML_ESCAPE = {
8
+ '&'.freeze => '&amp;'.freeze,
9
+ '>'.freeze => '&gt;'.freeze,
10
+ '<'.freeze => '&lt;'.freeze,
11
+ '"'.freeze => '&quot;'.freeze,
12
+ "'".freeze => '&#39;'.freeze
13
+ }
14
+ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
7
15
 
8
16
  # Return the size of an array or of an string
9
17
  def size(input)
10
-
11
18
  input.respond_to?(:size) ? input.size : 0
12
19
  end
13
20
 
@@ -27,32 +34,43 @@ module Liquid
27
34
  end
28
35
 
29
36
  def escape(input)
30
- CGI.escapeHTML(input) rescue input
37
+ CGI.escapeHTML(input).untaint rescue input
31
38
  end
39
+ alias_method :h, :escape
32
40
 
33
41
  def escape_once(input)
34
- ActionView::Helpers::TagHelper.escape_once(input)
35
- rescue NameError
36
- input
42
+ input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
37
43
  end
38
44
 
39
- alias_method :h, :escape
45
+ def url_encode(input)
46
+ CGI.escape(input) rescue input
47
+ end
48
+
49
+ def slice(input, offset, length=nil)
50
+ offset = Integer(offset)
51
+ length = length ? Integer(length) : 1
52
+
53
+ if input.is_a?(Array)
54
+ input.slice(offset, length) || []
55
+ else
56
+ input.to_s.slice(offset, length) || ''
57
+ end
58
+ end
40
59
 
41
60
  # Truncate a string down to x characters
42
- def truncate(input, length = 50, truncate_string = "...")
61
+ def truncate(input, length = 50, truncate_string = "...".freeze)
43
62
  if input.nil? then return end
44
63
  l = length.to_i - truncate_string.length
45
64
  l = 0 if l < 0
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
65
+ input.length > length.to_i ? input[0...l] + truncate_string : input
48
66
  end
49
67
 
50
- def truncatewords(input, words = 15, truncate_string = "...")
68
+ def truncatewords(input, words = 15, truncate_string = "...".freeze)
51
69
  if input.nil? then return end
52
70
  wordlist = input.to_s.split
53
71
  l = words.to_i - 1
54
72
  l = 0 if l < 0
55
- wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input
73
+ wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
56
74
  end
57
75
 
58
76
  # Split input string into an array of substrings separated by given pattern.
@@ -61,50 +79,72 @@ module Liquid
61
79
  # <div class="summary">{{ post | split '//' | first }}</div>
62
80
  #
63
81
  def split(input, pattern)
64
- input.split(pattern)
82
+ input.to_s.split(pattern)
83
+ end
84
+
85
+ def strip(input)
86
+ input.to_s.strip
87
+ end
88
+
89
+ def lstrip(input)
90
+ input.to_s.lstrip
91
+ end
92
+
93
+ def rstrip(input)
94
+ input.to_s.rstrip
65
95
  end
66
96
 
67
97
  def strip_html(input)
68
- input.to_s.gsub(/<script.*?<\/script>/m, '').gsub(/<!--.*?-->/m, '').gsub(/<style.*?<\/style>/m, '').gsub(/<.*?>/m, '')
98
+ empty = ''.freeze
99
+ input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty)
69
100
  end
70
101
 
71
102
  # Remove all newlines from the string
72
103
  def strip_newlines(input)
73
- input.to_s.gsub(/\r?\n/, '')
104
+ input.to_s.gsub(/\r?\n/, ''.freeze)
74
105
  end
75
106
 
76
107
  # Join elements of the array with certain character between them
77
- def join(input, glue = ' ')
78
- [input].flatten.join(glue)
108
+ def join(input, glue = ' '.freeze)
109
+ InputIterator.new(input).join(glue)
79
110
  end
80
111
 
81
112
  # Sort elements of the array
82
113
  # provide optional property with which to sort an array of hashes or drops
83
114
  def sort(input, property = nil)
84
- ary = [input].flatten
115
+ ary = InputIterator.new(input)
85
116
  if property.nil?
86
117
  ary.sort
87
- elsif ary.first.respond_to?('[]') and !ary.first[property].nil?
118
+ elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
88
119
  ary.sort {|a,b| a[property] <=> b[property] }
89
120
  elsif ary.first.respond_to?(property)
90
121
  ary.sort {|a,b| a.send(property) <=> b.send(property) }
91
122
  end
92
123
  end
93
124
 
125
+ # Remove duplicate elements from an array
126
+ # provide optional property with which to determine uniqueness
127
+ def uniq(input, property = nil)
128
+ ary = InputIterator.new(input)
129
+ if property.nil?
130
+ input.uniq
131
+ elsif input.first.respond_to?(:[])
132
+ input.uniq{ |a| a[property] }
133
+ end
134
+ end
135
+
94
136
  # Reverse the elements of an array
95
137
  def reverse(input)
96
- ary = [input].flatten
138
+ ary = InputIterator.new(input)
97
139
  ary.reverse
98
140
  end
99
141
 
100
142
  # map/collect on a given property
101
143
  def map(input, property)
102
- ary = [input].flatten
103
- ary.map do |e|
144
+ InputIterator.new(input).map do |e|
104
145
  e = e.call if e.is_a?(Proc)
105
- e = e.to_liquid if e.respond_to?(:to_liquid)
106
146
 
107
- if property == "to_liquid"
147
+ if property == "to_liquid".freeze
108
148
  e
109
149
  elsif e.respond_to?(:[])
110
150
  e[property]
@@ -113,23 +153,23 @@ module Liquid
113
153
  end
114
154
 
115
155
  # Replace occurrences of a string with another
116
- def replace(input, string, replacement = '')
156
+ def replace(input, string, replacement = ''.freeze)
117
157
  input.to_s.gsub(string, replacement.to_s)
118
158
  end
119
159
 
120
160
  # Replace the first occurrences of a string with another
121
- def replace_first(input, string, replacement = '')
161
+ def replace_first(input, string, replacement = ''.freeze)
122
162
  input.to_s.sub(string, replacement.to_s)
123
163
  end
124
164
 
125
165
  # remove a substring
126
166
  def remove(input, string)
127
- input.to_s.gsub(string, '')
167
+ input.to_s.gsub(string, ''.freeze)
128
168
  end
129
169
 
130
170
  # remove the first occurrences of a substring
131
171
  def remove_first(input, string)
132
- input.to_s.sub(string, '')
172
+ input.to_s.sub(string, ''.freeze)
133
173
  end
134
174
 
135
175
  # add one string to another
@@ -144,10 +184,10 @@ module Liquid
144
184
 
145
185
  # Add <br /> tags in front of all newlines in input string
146
186
  def newline_to_br(input)
147
- input.to_s.gsub(/\n/, "<br />\n")
187
+ input.to_s.gsub(/\n/, "<br />\n".freeze)
148
188
  end
149
189
 
150
- # Reformat a date
190
+ # Reformat a date using Ruby's core Time#strftime( string ) -> string
151
191
  #
152
192
  # %a - The abbreviated weekday name (``Sun'')
153
193
  # %A - The full weekday name (``Sunday'')
@@ -161,6 +201,7 @@ module Liquid
161
201
  # %m - Month of the year (01..12)
162
202
  # %M - Minute of the hour (00..59)
163
203
  # %p - Meridian indicator (``AM'' or ``PM'')
204
+ # %s - Number of seconds since 1970-01-01 00:00:00 UTC.
164
205
  # %S - Second of the minute (00..60)
165
206
  # %U - Week number of the current year,
166
207
  # starting with the first Sunday as the first
@@ -175,34 +216,14 @@ module Liquid
175
216
  # %Y - Year with century
176
217
  # %Z - Time zone name
177
218
  # %% - Literal ``%'' character
219
+ #
220
+ # See also: http://www.ruby-doc.org/core/Time.html#method-i-strftime
178
221
  def date(input, format)
222
+ return input if format.to_s.empty?
179
223
 
180
- if format.to_s.empty?
181
- return input.to_s
182
- end
183
-
184
- if ((input.is_a?(String) && !/^\d+$/.match(input.to_s).nil?) || input.is_a?(Integer)) && input.to_i > 0
185
- input = Time.at(input.to_i)
186
- end
224
+ return input unless date = to_date(input)
187
225
 
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
198
-
199
- if date.respond_to?(:strftime)
200
- date.strftime(format.to_s)
201
- else
202
- input
203
- end
204
- rescue
205
- input
226
+ date.strftime(format.to_s)
206
227
  end
207
228
 
208
229
  # Get the first element of the passed in array
@@ -247,6 +268,26 @@ module Liquid
247
268
  apply_operation(input, operand, :%)
248
269
  end
249
270
 
271
+ def round(input, n = 0)
272
+ result = to_number(input).round(to_number(n))
273
+ result = result.to_f if result.is_a?(BigDecimal)
274
+ result = result.to_i if n == 0
275
+ result
276
+ end
277
+
278
+ def ceil(input)
279
+ to_number(input).ceil.to_i
280
+ end
281
+
282
+ 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
289
+ end
290
+
250
291
  private
251
292
 
252
293
  def to_number(obj)
@@ -256,16 +297,63 @@ module Liquid
256
297
  when Numeric
257
298
  obj
258
299
  when String
259
- (obj.strip =~ /^\d+\.\d+$/) ? BigDecimal.new(obj) : obj.to_i
300
+ (obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
260
301
  else
261
302
  0
262
303
  end
263
304
  end
264
305
 
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
322
+
265
323
  def apply_operation(input, operand, operation)
266
324
  result = to_number(input).send(operation, to_number(operand))
267
325
  result.is_a?(BigDecimal) ? result.to_f : result
268
326
  end
327
+
328
+ class InputIterator
329
+ include Enumerable
330
+
331
+ def initialize(input)
332
+ @input = if input.is_a?(Array)
333
+ input.flatten
334
+ elsif input.is_a?(Hash)
335
+ [input]
336
+ elsif input.is_a?(Enumerable)
337
+ input
338
+ else
339
+ Array(input)
340
+ end
341
+ end
342
+
343
+ def join(glue)
344
+ to_a.join(glue)
345
+ end
346
+
347
+ def reverse
348
+ reverse_each.to_a
349
+ end
350
+
351
+ def each
352
+ @input.each do |e|
353
+ yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
354
+ end
355
+ end
356
+ end
269
357
  end
270
358
 
271
359
  Template.register_filter(StandardFilters)
@@ -11,6 +11,11 @@ module Liquid
11
11
  @@filters = []
12
12
  @@known_filters = Set.new
13
13
  @@known_methods = Set.new
14
+ @@strainer_class_cache = Hash.new do |hash, filters|
15
+ hash[filters] = Class.new(Strainer) do
16
+ filters.each { |f| include f }
17
+ end
18
+ end
14
19
 
15
20
  def initialize(context)
16
21
  @context = context
@@ -32,10 +37,13 @@ module Liquid
32
37
  end
33
38
  end
34
39
 
35
- def self.create(context)
36
- strainer = Strainer.new(context)
37
- @@filters.each { |m| strainer.extend(m) }
38
- strainer
40
+ def self.strainer_class_cache
41
+ @@strainer_class_cache
42
+ end
43
+
44
+ def self.create(context, filters = [])
45
+ filters = @@filters + filters
46
+ strainer_class_cache[filters].new(context)
39
47
  end
40
48
 
41
49
  def invoke(method, *args)
@@ -44,6 +52,8 @@ module Liquid
44
52
  else
45
53
  args.first
46
54
  end
55
+ rescue ::ArgumentError => e
56
+ raise Liquid::ArgumentError.new(e.message)
47
57
  end
48
58
 
49
59
  def invokable?(method)