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