liquid 2.6.3 → 3.0.0.rc1

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 (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)