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