liquid 2.6.3 → 3.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +42 -13
  3. data/README.md +27 -2
  4. data/lib/liquid.rb +11 -11
  5. data/lib/liquid/block.rb +75 -45
  6. data/lib/liquid/condition.rb +15 -11
  7. data/lib/liquid/context.rb +68 -29
  8. data/lib/liquid/document.rb +3 -3
  9. data/lib/liquid/drop.rb +17 -1
  10. data/lib/liquid/file_system.rb +17 -6
  11. data/lib/liquid/i18n.rb +39 -0
  12. data/lib/liquid/interrupts.rb +1 -1
  13. data/lib/liquid/lexer.rb +51 -0
  14. data/lib/liquid/locales/en.yml +22 -0
  15. data/lib/liquid/parser.rb +90 -0
  16. data/lib/liquid/standardfilters.rb +115 -52
  17. data/lib/liquid/strainer.rb +14 -4
  18. data/lib/liquid/tag.rb +42 -7
  19. data/lib/liquid/tags/assign.rb +10 -8
  20. data/lib/liquid/tags/break.rb +1 -1
  21. data/lib/liquid/tags/capture.rb +10 -8
  22. data/lib/liquid/tags/case.rb +13 -13
  23. data/lib/liquid/tags/comment.rb +9 -2
  24. data/lib/liquid/tags/continue.rb +1 -4
  25. data/lib/liquid/tags/cycle.rb +5 -7
  26. data/lib/liquid/tags/decrement.rb +3 -4
  27. data/lib/liquid/tags/for.rb +69 -36
  28. data/lib/liquid/tags/if.rb +52 -25
  29. data/lib/liquid/tags/ifchanged.rb +2 -2
  30. data/lib/liquid/tags/include.rb +8 -7
  31. data/lib/liquid/tags/increment.rb +4 -8
  32. data/lib/liquid/tags/raw.rb +3 -3
  33. data/lib/liquid/tags/table_row.rb +73 -0
  34. data/lib/liquid/tags/unless.rb +2 -4
  35. data/lib/liquid/template.rb +69 -10
  36. data/lib/liquid/utils.rb +13 -4
  37. data/lib/liquid/variable.rb +59 -8
  38. data/lib/liquid/version.rb +1 -1
  39. data/test/fixtures/en_locale.yml +9 -0
  40. data/test/{liquid → integration}/assign_test.rb +6 -0
  41. data/test/integration/blank_test.rb +106 -0
  42. data/test/{liquid → integration}/capture_test.rb +2 -2
  43. data/test/integration/context_test.rb +33 -0
  44. data/test/integration/drop_test.rb +245 -0
  45. data/test/{liquid → integration}/error_handling_test.rb +31 -2
  46. data/test/{liquid → integration}/filter_test.rb +7 -7
  47. data/test/{liquid → integration}/hash_ordering_test.rb +0 -0
  48. data/test/{liquid → integration}/output_test.rb +12 -12
  49. data/test/integration/parsing_quirks_test.rb +94 -0
  50. data/test/{liquid → integration}/security_test.rb +9 -9
  51. data/test/{liquid → integration}/standard_filter_test.rb +103 -33
  52. data/test/{liquid → integration}/tags/break_tag_test.rb +0 -0
  53. data/test/{liquid → integration}/tags/continue_tag_test.rb +0 -0
  54. data/test/{liquid → integration}/tags/for_tag_test.rb +78 -0
  55. data/test/{liquid → integration}/tags/if_else_tag_test.rb +1 -1
  56. data/test/integration/tags/include_tag_test.rb +212 -0
  57. data/test/{liquid → integration}/tags/increment_tag_test.rb +0 -0
  58. data/test/{liquid → integration}/tags/raw_tag_test.rb +1 -0
  59. data/test/{liquid → integration}/tags/standard_tag_test.rb +24 -22
  60. data/test/integration/tags/statements_test.rb +113 -0
  61. data/test/{liquid/tags/html_tag_test.rb → integration/tags/table_row_test.rb} +5 -5
  62. data/test/{liquid → integration}/tags/unless_else_tag_test.rb +0 -0
  63. data/test/{liquid → integration}/template_test.rb +66 -42
  64. data/test/integration/variable_test.rb +72 -0
  65. data/test/test_helper.rb +32 -7
  66. data/test/{liquid/block_test.rb → unit/block_unit_test.rb} +1 -1
  67. data/test/{liquid/condition_test.rb → unit/condition_unit_test.rb} +19 -1
  68. data/test/{liquid/context_test.rb → unit/context_unit_test.rb} +27 -19
  69. data/test/{liquid/file_system_test.rb → unit/file_system_unit_test.rb} +7 -1
  70. data/test/unit/i18n_unit_test.rb +37 -0
  71. data/test/unit/lexer_unit_test.rb +48 -0
  72. data/test/{liquid/module_ex_test.rb → unit/module_ex_unit_test.rb} +7 -7
  73. data/test/unit/parser_unit_test.rb +82 -0
  74. data/test/{liquid/regexp_test.rb → unit/regexp_unit_test.rb} +3 -3
  75. data/test/{liquid/strainer_test.rb → unit/strainer_unit_test.rb} +19 -1
  76. data/test/unit/tag_unit_test.rb +11 -0
  77. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  78. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  79. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  80. data/test/unit/template_unit_test.rb +69 -0
  81. data/test/unit/tokenizer_unit_test.rb +29 -0
  82. data/test/{liquid/variable_test.rb → unit/variable_unit_test.rb} +17 -67
  83. metadata +117 -73
  84. data/lib/extras/liquid_view.rb +0 -51
  85. data/lib/liquid/htmltags.rb +0 -73
  86. data/test/liquid/drop_test.rb +0 -180
  87. data/test/liquid/parsing_quirks_test.rb +0 -52
  88. data/test/liquid/tags/include_tag_test.rb +0 -166
  89. data/test/liquid/tags/statements_test.rb +0 -134
@@ -1,8 +1,8 @@
1
1
  module Liquid
2
2
  class Document < Block
3
- # we don't need markup to open this block
4
- def initialize(tokens)
5
- parse(tokens)
3
+ def self.parse(tokens, options={})
4
+ # we don't need markup to open this block
5
+ super(nil, nil, tokens, options)
6
6
  end
7
7
 
8
8
  # There isn't a real delimiter
data/lib/liquid/drop.rb CHANGED
@@ -44,17 +44,33 @@ module Liquid
44
44
  true
45
45
  end
46
46
 
47
+ def inspect
48
+ self.class.to_s
49
+ end
50
+
47
51
  def to_liquid
48
52
  self
49
53
  end
50
54
 
55
+ def to_s
56
+ self.class.name
57
+ end
58
+
51
59
  alias :[] :invoke_drop
52
60
 
53
61
  private
54
62
 
55
63
  # Check for method existence without invoking respond_to?, which creates symbols
56
64
  def self.invokable?(method_name)
57
- @invokable_methods ||= Set.new(["to_liquid"] + (public_instance_methods - Liquid::Drop.public_instance_methods).map(&:to_s))
65
+ unless @invokable_methods
66
+ blacklist = Liquid::Drop.public_instance_methods + [:each]
67
+ if include?(Enumerable)
68
+ blacklist += Enumerable.public_instance_methods
69
+ blacklist -= [:sort, :count, :first, :min, :max, :include?]
70
+ end
71
+ whitelist = [:to_liquid] + (public_instance_methods - blacklist)
72
+ @invokable_methods = Set.new(whitelist.map(&:to_s))
73
+ end
58
74
  @invokable_methods.include?(method_name.to_s)
59
75
  end
60
76
  end
@@ -31,11 +31,22 @@ module Liquid
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
+ # Optionally in the second argument you can specify a custom pattern for template filenames.
35
+ # The Kernel::sprintf format specification is used.
36
+ # Default pattern is "_%s.liquid".
37
+ #
38
+ # Example:
39
+ #
40
+ # file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
41
+ #
42
+ # file_system.full_path("index") # => "/some/path/index.html"
43
+ #
34
44
  class LocalFileSystem
35
45
  attr_accessor :root
36
46
 
37
- def initialize(root)
47
+ def initialize(root, pattern = "_%s.liquid".freeze)
38
48
  @root = root
49
+ @pattern = pattern
39
50
  end
40
51
 
41
52
  def read_template_file(template_path, context)
@@ -46,15 +57,15 @@ module Liquid
46
57
  end
47
58
 
48
59
  def full_path(template_path)
49
- raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/
60
+ raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
50
61
 
51
- full_path = if template_path.include?('/')
52
- File.join(root, File.dirname(template_path), "_#{File.basename(template_path)}.liquid")
62
+ full_path = if template_path.include?('/'.freeze)
63
+ File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
53
64
  else
54
- File.join(root, "_#{template_path}.liquid")
65
+ File.join(root, @pattern % template_path)
55
66
  end
56
67
 
57
- raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(root)}/
68
+ raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/
58
69
 
59
70
  full_path
60
71
  end
@@ -0,0 +1,39 @@
1
+ require 'yaml'
2
+
3
+ module Liquid
4
+ class I18n
5
+ DEFAULT_LOCALE = File.join(File.expand_path(File.dirname(__FILE__)), "locales", "en.yml")
6
+
7
+ class TranslationError < StandardError
8
+ end
9
+
10
+ attr_reader :path
11
+
12
+ def initialize(path = DEFAULT_LOCALE)
13
+ @path = path
14
+ end
15
+
16
+ def translate(name, vars = {})
17
+ interpolate(deep_fetch_translation(name), vars)
18
+ end
19
+ alias_method :t, :translate
20
+
21
+ def locale
22
+ @locale ||= YAML.load_file(@path)
23
+ end
24
+
25
+ private
26
+ def interpolate(name, vars)
27
+ name.gsub(/%\{(\w+)\}/) {
28
+ # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
29
+ "#{vars[$1.to_sym]}"
30
+ }
31
+ end
32
+
33
+ def deep_fetch_translation(name)
34
+ name.split('.'.freeze).reduce(locale) do |level, cur|
35
+ level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -5,7 +5,7 @@ module Liquid
5
5
  attr_reader :message
6
6
 
7
7
  def initialize(message=nil)
8
- @message = message || "interrupt"
8
+ @message = message || "interrupt".freeze
9
9
  end
10
10
  end
11
11
 
@@ -0,0 +1,51 @@
1
+ require "strscan"
2
+ module Liquid
3
+ class Lexer
4
+ SPECIALS = {
5
+ '|'.freeze => :pipe,
6
+ '.'.freeze => :dot,
7
+ ':'.freeze => :colon,
8
+ ','.freeze => :comma,
9
+ '['.freeze => :open_square,
10
+ ']'.freeze => :close_square,
11
+ '('.freeze => :open_round,
12
+ ')'.freeze => :close_round
13
+ }
14
+ IDENTIFIER = /[\w\-?!]+/
15
+ SINGLE_STRING_LITERAL = /'[^\']*'/
16
+ DOUBLE_STRING_LITERAL = /"[^\"]*"/
17
+ NUMBER_LITERAL = /-?\d+(\.\d+)?/
18
+ DOTDOT = /\.\./
19
+ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
20
+
21
+ def initialize(input)
22
+ @ss = StringScanner.new(input.rstrip)
23
+ end
24
+
25
+ def tokenize
26
+ @output = []
27
+
28
+ while !@ss.eos?
29
+ @ss.skip(/\s*/)
30
+ tok = case
31
+ when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
32
+ when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
33
+ when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
34
+ when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
35
+ when t = @ss.scan(IDENTIFIER) then [:id, t]
36
+ when t = @ss.scan(DOTDOT) then [:dotdot, t]
37
+ else
38
+ c = @ss.getch
39
+ if s = SPECIALS[c]
40
+ [s,c]
41
+ else
42
+ raise SyntaxError, "Unexpected character #{c}"
43
+ end
44
+ end
45
+ @output << tok
46
+ end
47
+
48
+ @output << [:end_of_string]
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,22 @@
1
+ ---
2
+ errors:
3
+ syntax:
4
+ assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
5
+ capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
6
+ case: "Syntax Error in 'case' - Valid syntax: case [condition]"
7
+ case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
8
+ case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
9
+ cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
10
+ for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"
11
+ for_invalid_in: "For loops require an 'in' clause"
12
+ for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
13
+ if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
14
+ include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
15
+ unknown_tag: "Unknown tag '%{tag}'"
16
+ 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
+ tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
19
+ variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
20
+ tag_never_closed: "'%{block_name}' tag was never closed"
21
+ meta_syntax_error: "Liquid syntax error: #{e.message}"
22
+ table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
@@ -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
@@ -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
 
@@ -31,28 +38,25 @@ module Liquid
31
38
  end
32
39
 
33
40
  def escape_once(input)
34
- ActionView::Helpers::TagHelper.escape_once(input)
35
- rescue NameError
36
- input
41
+ input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
37
42
  end
38
43
 
39
44
  alias_method :h, :escape
40
45
 
41
46
  # Truncate a string down to x characters
42
- def truncate(input, length = 50, truncate_string = "...")
47
+ def truncate(input, length = 50, truncate_string = "...".freeze)
43
48
  if input.nil? then return end
44
49
  l = length.to_i - truncate_string.length
45
50
  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
51
+ input.length > length.to_i ? input[0...l] + truncate_string : input
48
52
  end
49
53
 
50
- def truncatewords(input, words = 15, truncate_string = "...")
54
+ def truncatewords(input, words = 15, truncate_string = "...".freeze)
51
55
  if input.nil? then return end
52
56
  wordlist = input.to_s.split
53
57
  l = words.to_i - 1
54
58
  l = 0 if l < 0
55
- wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input
59
+ wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
56
60
  end
57
61
 
58
62
  # Split input string into an array of substrings separated by given pattern.
@@ -64,27 +68,40 @@ module Liquid
64
68
  input.split(pattern)
65
69
  end
66
70
 
71
+ def strip(input)
72
+ input.to_s.strip
73
+ end
74
+
75
+ def lstrip(input)
76
+ input.to_s.lstrip
77
+ end
78
+
79
+ def rstrip(input)
80
+ input.to_s.rstrip
81
+ end
82
+
67
83
  def strip_html(input)
68
- input.to_s.gsub(/<script.*?<\/script>/m, '').gsub(/<!--.*?-->/m, '').gsub(/<style.*?<\/style>/m, '').gsub(/<.*?>/m, '')
84
+ empty = ''.freeze
85
+ input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty)
69
86
  end
70
87
 
71
88
  # Remove all newlines from the string
72
89
  def strip_newlines(input)
73
- input.to_s.gsub(/\r?\n/, '')
90
+ input.to_s.gsub(/\r?\n/, ''.freeze)
74
91
  end
75
92
 
76
93
  # Join elements of the array with certain character between them
77
- def join(input, glue = ' ')
78
- [input].flatten.join(glue)
94
+ def join(input, glue = ' '.freeze)
95
+ InputIterator.new(input).join(glue)
79
96
  end
80
97
 
81
98
  # Sort elements of the array
82
99
  # provide optional property with which to sort an array of hashes or drops
83
100
  def sort(input, property = nil)
84
- ary = [input].flatten
101
+ ary = InputIterator.new(input)
85
102
  if property.nil?
86
103
  ary.sort
87
- elsif ary.first.respond_to?('[]') and !ary.first[property].nil?
104
+ elsif ary.first.respond_to?('[]'.freeze) && !ary.first[property].nil?
88
105
  ary.sort {|a,b| a[property] <=> b[property] }
89
106
  elsif ary.first.respond_to?(property)
90
107
  ary.sort {|a,b| a.send(property) <=> b.send(property) }
@@ -93,18 +110,16 @@ module Liquid
93
110
 
94
111
  # Reverse the elements of an array
95
112
  def reverse(input)
96
- ary = [input].flatten
113
+ ary = InputIterator.new(input)
97
114
  ary.reverse
98
115
  end
99
116
 
100
117
  # map/collect on a given property
101
118
  def map(input, property)
102
- ary = [input].flatten
103
- ary.map do |e|
119
+ InputIterator.new(input).map do |e|
104
120
  e = e.call if e.is_a?(Proc)
105
- e = e.to_liquid if e.respond_to?(:to_liquid)
106
121
 
107
- if property == "to_liquid"
122
+ if property == "to_liquid".freeze
108
123
  e
109
124
  elsif e.respond_to?(:[])
110
125
  e[property]
@@ -113,23 +128,23 @@ module Liquid
113
128
  end
114
129
 
115
130
  # Replace occurrences of a string with another
116
- def replace(input, string, replacement = '')
131
+ def replace(input, string, replacement = ''.freeze)
117
132
  input.to_s.gsub(string, replacement.to_s)
118
133
  end
119
134
 
120
135
  # Replace the first occurrences of a string with another
121
- def replace_first(input, string, replacement = '')
136
+ def replace_first(input, string, replacement = ''.freeze)
122
137
  input.to_s.sub(string, replacement.to_s)
123
138
  end
124
139
 
125
140
  # remove a substring
126
141
  def remove(input, string)
127
- input.to_s.gsub(string, '')
142
+ input.to_s.gsub(string, ''.freeze)
128
143
  end
129
144
 
130
145
  # remove the first occurrences of a substring
131
146
  def remove_first(input, string)
132
- input.to_s.sub(string, '')
147
+ input.to_s.sub(string, ''.freeze)
133
148
  end
134
149
 
135
150
  # add one string to another
@@ -144,10 +159,10 @@ module Liquid
144
159
 
145
160
  # Add <br /> tags in front of all newlines in input string
146
161
  def newline_to_br(input)
147
- input.to_s.gsub(/\n/, "<br />\n")
162
+ input.to_s.gsub(/\n/, "<br />\n".freeze)
148
163
  end
149
164
 
150
- # Reformat a date
165
+ # Reformat a date using Ruby's core Time#strftime( string ) -> string
151
166
  #
152
167
  # %a - The abbreviated weekday name (``Sun'')
153
168
  # %A - The full weekday name (``Sunday'')
@@ -161,6 +176,7 @@ module Liquid
161
176
  # %m - Month of the year (01..12)
162
177
  # %M - Minute of the hour (00..59)
163
178
  # %p - Meridian indicator (``AM'' or ``PM'')
179
+ # %s - Number of seconds since 1970-01-01 00:00:00 UTC.
164
180
  # %S - Second of the minute (00..60)
165
181
  # %U - Week number of the current year,
166
182
  # starting with the first Sunday as the first
@@ -175,34 +191,14 @@ module Liquid
175
191
  # %Y - Year with century
176
192
  # %Z - Time zone name
177
193
  # %% - Literal ``%'' character
194
+ #
195
+ # See also: http://www.ruby-doc.org/core/Time.html#method-i-strftime
178
196
  def date(input, format)
197
+ return input if format.to_s.empty?
179
198
 
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
199
+ return input unless date = to_date(input)
187
200
 
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
201
+ date.strftime(format.to_s)
206
202
  end
207
203
 
208
204
  # Get the first element of the passed in array
@@ -247,6 +243,26 @@ module Liquid
247
243
  apply_operation(input, operand, :%)
248
244
  end
249
245
 
246
+ def round(input, n = 0)
247
+ result = to_number(input).round(to_number(n))
248
+ result = result.to_f if result.is_a?(BigDecimal)
249
+ result = result.to_i if n == 0
250
+ result
251
+ end
252
+
253
+ def ceil(input)
254
+ to_number(input).ceil.to_i
255
+ end
256
+
257
+ def floor(input)
258
+ to_number(input).floor.to_i
259
+ end
260
+
261
+ def default(input, default_value = "".freeze)
262
+ is_blank = input.respond_to?(:empty?) ? input.empty? : !input
263
+ is_blank ? default_value : input
264
+ end
265
+
250
266
  private
251
267
 
252
268
  def to_number(obj)
@@ -256,16 +272,63 @@ module Liquid
256
272
  when Numeric
257
273
  obj
258
274
  when String
259
- (obj.strip =~ /^\d+\.\d+$/) ? BigDecimal.new(obj) : obj.to_i
275
+ (obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
260
276
  else
261
277
  0
262
278
  end
263
279
  end
264
280
 
281
+ def to_date(obj)
282
+ return obj if obj.respond_to?(:strftime)
283
+
284
+ case obj
285
+ when 'now'.freeze, 'today'.freeze
286
+ Time.now
287
+ when /\A\d+\z/, Integer
288
+ Time.at(obj.to_i)
289
+ when String
290
+ Time.parse(obj)
291
+ else
292
+ nil
293
+ end
294
+ rescue ArgumentError
295
+ nil
296
+ end
297
+
265
298
  def apply_operation(input, operand, operation)
266
299
  result = to_number(input).send(operation, to_number(operand))
267
300
  result.is_a?(BigDecimal) ? result.to_f : result
268
301
  end
302
+
303
+ class InputIterator
304
+ include Enumerable
305
+
306
+ def initialize(input)
307
+ @input = if input.is_a?(Array)
308
+ input.flatten
309
+ elsif input.is_a?(Hash)
310
+ [input]
311
+ elsif input.is_a?(Enumerable)
312
+ input
313
+ else
314
+ Array(input)
315
+ end
316
+ end
317
+
318
+ def join(glue)
319
+ to_a.join(glue)
320
+ end
321
+
322
+ def reverse
323
+ reverse_each.to_a
324
+ end
325
+
326
+ def each
327
+ @input.each do |e|
328
+ yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
329
+ end
330
+ end
331
+ end
269
332
  end
270
333
 
271
334
  Template.register_filter(StandardFilters)