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.
- checksums.yaml +4 -4
- data/History.md +42 -13
- data/README.md +27 -2
- data/lib/liquid.rb +11 -11
- data/lib/liquid/block.rb +75 -45
- data/lib/liquid/condition.rb +15 -11
- data/lib/liquid/context.rb +68 -29
- data/lib/liquid/document.rb +3 -3
- data/lib/liquid/drop.rb +17 -1
- data/lib/liquid/file_system.rb +17 -6
- data/lib/liquid/i18n.rb +39 -0
- data/lib/liquid/interrupts.rb +1 -1
- data/lib/liquid/lexer.rb +51 -0
- data/lib/liquid/locales/en.yml +22 -0
- data/lib/liquid/parser.rb +90 -0
- data/lib/liquid/standardfilters.rb +115 -52
- data/lib/liquid/strainer.rb +14 -4
- data/lib/liquid/tag.rb +42 -7
- data/lib/liquid/tags/assign.rb +10 -8
- data/lib/liquid/tags/break.rb +1 -1
- data/lib/liquid/tags/capture.rb +10 -8
- data/lib/liquid/tags/case.rb +13 -13
- data/lib/liquid/tags/comment.rb +9 -2
- data/lib/liquid/tags/continue.rb +1 -4
- data/lib/liquid/tags/cycle.rb +5 -7
- data/lib/liquid/tags/decrement.rb +3 -4
- data/lib/liquid/tags/for.rb +69 -36
- data/lib/liquid/tags/if.rb +52 -25
- data/lib/liquid/tags/ifchanged.rb +2 -2
- data/lib/liquid/tags/include.rb +8 -7
- data/lib/liquid/tags/increment.rb +4 -8
- data/lib/liquid/tags/raw.rb +3 -3
- data/lib/liquid/tags/table_row.rb +73 -0
- data/lib/liquid/tags/unless.rb +2 -4
- data/lib/liquid/template.rb +69 -10
- data/lib/liquid/utils.rb +13 -4
- data/lib/liquid/variable.rb +59 -8
- data/lib/liquid/version.rb +1 -1
- data/test/fixtures/en_locale.yml +9 -0
- data/test/{liquid → integration}/assign_test.rb +6 -0
- data/test/integration/blank_test.rb +106 -0
- data/test/{liquid → integration}/capture_test.rb +2 -2
- data/test/integration/context_test.rb +33 -0
- data/test/integration/drop_test.rb +245 -0
- data/test/{liquid → integration}/error_handling_test.rb +31 -2
- data/test/{liquid → integration}/filter_test.rb +7 -7
- data/test/{liquid → integration}/hash_ordering_test.rb +0 -0
- data/test/{liquid → integration}/output_test.rb +12 -12
- data/test/integration/parsing_quirks_test.rb +94 -0
- data/test/{liquid → integration}/security_test.rb +9 -9
- data/test/{liquid → integration}/standard_filter_test.rb +103 -33
- data/test/{liquid → integration}/tags/break_tag_test.rb +0 -0
- data/test/{liquid → integration}/tags/continue_tag_test.rb +0 -0
- data/test/{liquid → integration}/tags/for_tag_test.rb +78 -0
- data/test/{liquid → integration}/tags/if_else_tag_test.rb +1 -1
- data/test/integration/tags/include_tag_test.rb +212 -0
- data/test/{liquid → integration}/tags/increment_tag_test.rb +0 -0
- data/test/{liquid → integration}/tags/raw_tag_test.rb +1 -0
- data/test/{liquid → integration}/tags/standard_tag_test.rb +24 -22
- data/test/integration/tags/statements_test.rb +113 -0
- data/test/{liquid/tags/html_tag_test.rb → integration/tags/table_row_test.rb} +5 -5
- data/test/{liquid → integration}/tags/unless_else_tag_test.rb +0 -0
- data/test/{liquid → integration}/template_test.rb +66 -42
- data/test/integration/variable_test.rb +72 -0
- data/test/test_helper.rb +32 -7
- data/test/{liquid/block_test.rb → unit/block_unit_test.rb} +1 -1
- data/test/{liquid/condition_test.rb → unit/condition_unit_test.rb} +19 -1
- data/test/{liquid/context_test.rb → unit/context_unit_test.rb} +27 -19
- data/test/{liquid/file_system_test.rb → unit/file_system_unit_test.rb} +7 -1
- data/test/unit/i18n_unit_test.rb +37 -0
- data/test/unit/lexer_unit_test.rb +48 -0
- data/test/{liquid/module_ex_test.rb → unit/module_ex_unit_test.rb} +7 -7
- data/test/unit/parser_unit_test.rb +82 -0
- data/test/{liquid/regexp_test.rb → unit/regexp_unit_test.rb} +3 -3
- data/test/{liquid/strainer_test.rb → unit/strainer_unit_test.rb} +19 -1
- data/test/unit/tag_unit_test.rb +11 -0
- data/test/unit/tags/case_tag_unit_test.rb +10 -0
- data/test/unit/tags/for_tag_unit_test.rb +13 -0
- data/test/unit/tags/if_tag_unit_test.rb +8 -0
- data/test/unit/template_unit_test.rb +69 -0
- data/test/unit/tokenizer_unit_test.rb +29 -0
- data/test/{liquid/variable_test.rb → unit/variable_unit_test.rb} +17 -67
- metadata +117 -73
- data/lib/extras/liquid_view.rb +0 -51
- data/lib/liquid/htmltags.rb +0 -73
- data/test/liquid/drop_test.rb +0 -180
- data/test/liquid/parsing_quirks_test.rb +0 -52
- data/test/liquid/tags/include_tag_test.rb +0 -166
- data/test/liquid/tags/statements_test.rb +0 -134
data/lib/liquid/document.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module Liquid
|
2
2
|
class Document < Block
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|
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
|
data/lib/liquid/file_system.rb
CHANGED
@@ -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 =~
|
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),
|
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,
|
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) =~
|
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
|
data/lib/liquid/i18n.rb
ADDED
@@ -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
|
data/lib/liquid/interrupts.rb
CHANGED
data/lib/liquid/lexer.rb
ADDED
@@ -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 => '&'.freeze,
|
9
|
+
'>'.freeze => '>'.freeze,
|
10
|
+
'<'.freeze => '<'.freeze,
|
11
|
+
'"'.freeze => '"'.freeze,
|
12
|
+
"'".freeze => '''.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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
101
|
+
ary = InputIterator.new(input)
|
85
102
|
if property.nil?
|
86
103
|
ary.sort
|
87
|
-
elsif ary.first.respond_to?('[]')
|
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 =
|
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
|
-
|
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
|
-
|
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
|
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 =~
|
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)
|