liquid 3.0.6 → 4.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 +98 -58
- data/README.md +31 -0
- data/lib/liquid/block.rb +31 -124
- data/lib/liquid/block_body.rb +75 -59
- data/lib/liquid/condition.rb +23 -22
- data/lib/liquid/context.rb +50 -46
- data/lib/liquid/document.rb +19 -9
- data/lib/liquid/drop.rb +17 -16
- data/lib/liquid/errors.rb +20 -24
- data/lib/liquid/expression.rb +15 -3
- data/lib/liquid/extensions.rb +13 -7
- data/lib/liquid/file_system.rb +11 -11
- data/lib/liquid/forloop_drop.rb +42 -0
- data/lib/liquid/i18n.rb +5 -5
- data/lib/liquid/interrupts.rb +1 -2
- data/lib/liquid/lexer.rb +6 -4
- data/lib/liquid/locales/en.yml +5 -1
- data/lib/liquid/parse_context.rb +37 -0
- data/lib/liquid/parser_switching.rb +4 -4
- data/lib/liquid/profiler/hooks.rb +7 -7
- data/lib/liquid/profiler.rb +18 -19
- data/lib/liquid/range_lookup.rb +16 -1
- data/lib/liquid/resource_limits.rb +23 -0
- data/lib/liquid/standardfilters.rb +121 -61
- data/lib/liquid/strainer.rb +14 -7
- data/lib/liquid/tablerowloop_drop.rb +62 -0
- data/lib/liquid/tag.rb +9 -8
- data/lib/liquid/tags/assign.rb +17 -4
- data/lib/liquid/tags/break.rb +0 -3
- data/lib/liquid/tags/capture.rb +1 -1
- data/lib/liquid/tags/case.rb +19 -12
- data/lib/liquid/tags/comment.rb +2 -2
- data/lib/liquid/tags/cycle.rb +6 -6
- data/lib/liquid/tags/decrement.rb +1 -4
- data/lib/liquid/tags/for.rb +95 -75
- data/lib/liquid/tags/if.rb +49 -44
- data/lib/liquid/tags/ifchanged.rb +0 -2
- data/lib/liquid/tags/include.rb +61 -52
- data/lib/liquid/tags/raw.rb +32 -4
- data/lib/liquid/tags/table_row.rb +12 -30
- data/lib/liquid/tags/unless.rb +3 -4
- data/lib/liquid/template.rb +42 -54
- data/lib/liquid/tokenizer.rb +31 -0
- data/lib/liquid/utils.rb +52 -8
- data/lib/liquid/variable.rb +46 -45
- data/lib/liquid/variable_lookup.rb +7 -5
- data/lib/liquid/version.rb +1 -1
- data/lib/liquid.rb +9 -7
- data/test/integration/assign_test.rb +8 -8
- data/test/integration/blank_test.rb +14 -14
- data/test/integration/context_test.rb +2 -2
- data/test/integration/document_test.rb +19 -0
- data/test/integration/drop_test.rb +42 -40
- data/test/integration/error_handling_test.rb +99 -46
- data/test/integration/filter_test.rb +60 -20
- data/test/integration/hash_ordering_test.rb +9 -9
- data/test/integration/output_test.rb +26 -27
- data/test/integration/parsing_quirks_test.rb +15 -13
- data/test/integration/render_profiling_test.rb +20 -20
- data/test/integration/security_test.rb +9 -7
- data/test/integration/standard_filter_test.rb +179 -40
- data/test/integration/tags/break_tag_test.rb +1 -2
- data/test/integration/tags/continue_tag_test.rb +0 -1
- data/test/integration/tags/for_tag_test.rb +133 -98
- data/test/integration/tags/if_else_tag_test.rb +75 -77
- data/test/integration/tags/include_tag_test.rb +34 -30
- data/test/integration/tags/increment_tag_test.rb +10 -11
- data/test/integration/tags/raw_tag_test.rb +7 -1
- data/test/integration/tags/standard_tag_test.rb +121 -122
- data/test/integration/tags/statements_test.rb +3 -5
- data/test/integration/tags/table_row_test.rb +20 -19
- data/test/integration/tags/unless_else_tag_test.rb +6 -6
- data/test/integration/template_test.rb +190 -49
- data/test/integration/trim_mode_test.rb +525 -0
- data/test/integration/variable_test.rb +23 -13
- data/test/test_helper.rb +33 -5
- data/test/unit/block_unit_test.rb +8 -5
- data/test/unit/condition_unit_test.rb +86 -77
- data/test/unit/context_unit_test.rb +48 -57
- data/test/unit/file_system_unit_test.rb +3 -3
- data/test/unit/i18n_unit_test.rb +2 -2
- data/test/unit/lexer_unit_test.rb +11 -8
- data/test/unit/parser_unit_test.rb +2 -2
- data/test/unit/regexp_unit_test.rb +1 -1
- data/test/unit/strainer_unit_test.rb +80 -1
- data/test/unit/tag_unit_test.rb +7 -2
- data/test/unit/tags/case_tag_unit_test.rb +1 -1
- data/test/unit/tags/for_tag_unit_test.rb +2 -2
- data/test/unit/tags/if_tag_unit_test.rb +1 -1
- data/test/unit/template_unit_test.rb +14 -5
- data/test/unit/tokenizer_unit_test.rb +24 -7
- data/test/unit/variable_unit_test.rb +60 -43
- metadata +19 -14
- data/lib/liquid/module_ex.rb +0 -62
- data/lib/liquid/token.rb +0 -18
- data/test/unit/module_ex_unit_test.rb +0 -87
- /data/{MIT-LICENSE → LICENSE} +0 -0
data/lib/liquid/i18n.rb
CHANGED
@@ -2,10 +2,9 @@ require 'yaml'
|
|
2
2
|
|
3
3
|
module Liquid
|
4
4
|
class I18n
|
5
|
-
DEFAULT_LOCALE = File.join(File.expand_path(
|
5
|
+
DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml")
|
6
6
|
|
7
|
-
|
8
|
-
end
|
7
|
+
TranslationError = Class.new(StandardError)
|
9
8
|
|
10
9
|
attr_reader :path
|
11
10
|
|
@@ -23,11 +22,12 @@ module Liquid
|
|
23
22
|
end
|
24
23
|
|
25
24
|
private
|
25
|
+
|
26
26
|
def interpolate(name, vars)
|
27
|
-
name.gsub(/%\{(\w+)\}/)
|
27
|
+
name.gsub(/%\{(\w+)\}/) do
|
28
28
|
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
|
29
29
|
"#{vars[$1.to_sym]}"
|
30
|
-
|
30
|
+
end
|
31
31
|
end
|
32
32
|
|
33
33
|
def deep_fetch_translation(name)
|
data/lib/liquid/interrupts.rb
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
module Liquid
|
2
|
-
|
3
2
|
# An interrupt is any command that breaks processing of a block (ex: a for loop).
|
4
3
|
class Interrupt
|
5
4
|
attr_reader :message
|
6
5
|
|
7
|
-
def initialize(message=nil)
|
6
|
+
def initialize(message = nil)
|
8
7
|
@message = message || "interrupt".freeze
|
9
8
|
end
|
10
9
|
end
|
data/lib/liquid/lexer.rb
CHANGED
@@ -9,9 +9,11 @@ module Liquid
|
|
9
9
|
'['.freeze => :open_square,
|
10
10
|
']'.freeze => :close_square,
|
11
11
|
'('.freeze => :open_round,
|
12
|
-
')'.freeze => :close_round
|
12
|
+
')'.freeze => :close_round,
|
13
|
+
'?'.freeze => :question,
|
14
|
+
'-'.freeze => :dash
|
13
15
|
}
|
14
|
-
IDENTIFIER = /[\w
|
16
|
+
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
15
17
|
SINGLE_STRING_LITERAL = /'[^\']*'/
|
16
18
|
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
17
19
|
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
@@ -25,7 +27,7 @@ module Liquid
|
|
25
27
|
def tokenize
|
26
28
|
@output = []
|
27
29
|
|
28
|
-
|
30
|
+
until @ss.eos?
|
29
31
|
@ss.skip(/\s*/)
|
30
32
|
tok = case
|
31
33
|
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
|
@@ -37,7 +39,7 @@ module Liquid
|
|
37
39
|
else
|
38
40
|
c = @ss.getch
|
39
41
|
if s = SPECIALS[c]
|
40
|
-
[s,c]
|
42
|
+
[s, c]
|
41
43
|
else
|
42
44
|
raise SyntaxError, "Unexpected character #{c}"
|
43
45
|
end
|
data/lib/liquid/locales/en.yml
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
---
|
2
2
|
errors:
|
3
3
|
syntax:
|
4
|
+
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
|
4
5
|
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
|
5
6
|
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
|
6
7
|
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
|
@@ -14,9 +15,12 @@
|
|
14
15
|
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
|
15
16
|
unknown_tag: "Unknown tag '%{tag}'"
|
16
17
|
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
|
+
unexpected_else: "%{block_name} tag does not expect 'else' tag"
|
19
|
+
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
|
18
20
|
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|
19
21
|
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
|
20
22
|
tag_never_closed: "'%{block_name}' tag was never closed"
|
21
23
|
meta_syntax_error: "Liquid syntax error: #{e.message}"
|
22
24
|
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
25
|
+
argument:
|
26
|
+
include: "Argument error in tag 'include' - Illegal template name"
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Liquid
|
2
|
+
class ParseContext
|
3
|
+
attr_accessor :locale, :line_number, :trim_whitespace
|
4
|
+
attr_reader :partial, :warnings, :error_mode
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
@template_options = options ? options.dup : {}
|
8
|
+
@locale = @template_options[:locale] ||= I18n.new
|
9
|
+
@warnings = []
|
10
|
+
self.partial = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](option_key)
|
14
|
+
@options[option_key]
|
15
|
+
end
|
16
|
+
|
17
|
+
def partial=(value)
|
18
|
+
@partial = value
|
19
|
+
@options = value ? partial_options : @template_options
|
20
|
+
@error_mode = @options[:error_mode] || Template.error_mode
|
21
|
+
value
|
22
|
+
end
|
23
|
+
|
24
|
+
def partial_options
|
25
|
+
@partial_options ||= begin
|
26
|
+
dont_pass = @template_options[:include_options_blacklist]
|
27
|
+
if dont_pass == true
|
28
|
+
{ locale: locale }
|
29
|
+
elsif dont_pass.is_a?(Array)
|
30
|
+
@template_options.reject { |k, v| dont_pass.include?(k) }
|
31
|
+
else
|
32
|
+
@template_options
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -1,25 +1,25 @@
|
|
1
1
|
module Liquid
|
2
2
|
module ParserSwitching
|
3
3
|
def parse_with_selected_parser(markup)
|
4
|
-
case
|
4
|
+
case parse_context.error_mode
|
5
5
|
when :strict then strict_parse_with_error_context(markup)
|
6
6
|
when :lax then lax_parse(markup)
|
7
7
|
when :warn
|
8
8
|
begin
|
9
9
|
return strict_parse_with_error_context(markup)
|
10
10
|
rescue SyntaxError => e
|
11
|
-
e
|
12
|
-
@warnings ||= []
|
13
|
-
@warnings << e
|
11
|
+
parse_context.warnings << e
|
14
12
|
return lax_parse(markup)
|
15
13
|
end
|
16
14
|
end
|
17
15
|
end
|
18
16
|
|
19
17
|
private
|
18
|
+
|
20
19
|
def strict_parse_with_error_context(markup)
|
21
20
|
strict_parse(markup)
|
22
21
|
rescue SyntaxError => e
|
22
|
+
e.line_number = line_number
|
23
23
|
e.markup_context = markup_context(markup)
|
24
24
|
raise e
|
25
25
|
end
|
@@ -1,18 +1,18 @@
|
|
1
1
|
module Liquid
|
2
|
-
class
|
3
|
-
def
|
4
|
-
Profiler.
|
5
|
-
|
2
|
+
class BlockBody
|
3
|
+
def render_node_with_profiling(node, context)
|
4
|
+
Profiler.profile_node_render(node) do
|
5
|
+
render_node_without_profiling(node, context)
|
6
6
|
end
|
7
7
|
end
|
8
8
|
|
9
|
-
alias_method :
|
10
|
-
alias_method :
|
9
|
+
alias_method :render_node_without_profiling, :render_node
|
10
|
+
alias_method :render_node, :render_node_with_profiling
|
11
11
|
end
|
12
12
|
|
13
13
|
class Include < Tag
|
14
14
|
def render_with_profiling(context)
|
15
|
-
Profiler.profile_children(@
|
15
|
+
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
|
16
16
|
render_without_profiling(context)
|
17
17
|
end
|
18
18
|
end
|
data/lib/liquid/profiler.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
|
1
|
+
require 'liquid/profiler/hooks'
|
2
2
|
|
3
|
+
module Liquid
|
3
4
|
# Profiler enables support for profiling template rendering to help track down performance issues.
|
4
5
|
#
|
5
|
-
# To enable profiling,
|
6
|
-
#
|
6
|
+
# To enable profiling, first require 'liquid/profiler'.
|
7
|
+
# Then, to profile a parse/render cycle, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>.
|
8
|
+
# After <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
|
7
9
|
# class via the <tt>Liquid::Template#profiler</tt> method.
|
8
10
|
#
|
9
11
|
# template = Liquid::Template.parse(template_content, profile: true)
|
@@ -17,7 +19,7 @@ module Liquid
|
|
17
19
|
# inside of <tt>{% include %}</tt> tags.
|
18
20
|
#
|
19
21
|
# profile.each do |node|
|
20
|
-
# # Access to the
|
22
|
+
# # Access to the node itself
|
21
23
|
# node.code
|
22
24
|
#
|
23
25
|
# # Which template and line number of this node.
|
@@ -44,17 +46,15 @@ module Liquid
|
|
44
46
|
class Timing
|
45
47
|
attr_reader :code, :partial, :line_number, :children
|
46
48
|
|
47
|
-
def initialize(
|
48
|
-
@code =
|
49
|
+
def initialize(node, partial)
|
50
|
+
@code = node.respond_to?(:raw) ? node.raw : node
|
49
51
|
@partial = partial
|
50
|
-
@line_number =
|
52
|
+
@line_number = node.respond_to?(:line_number) ? node.line_number : nil
|
51
53
|
@children = []
|
52
54
|
end
|
53
55
|
|
54
|
-
def self.start(
|
55
|
-
new(
|
56
|
-
t.start
|
57
|
-
end
|
56
|
+
def self.start(node, partial)
|
57
|
+
new(node, partial).tap(&:start)
|
58
58
|
end
|
59
59
|
|
60
60
|
def start
|
@@ -70,11 +70,11 @@ module Liquid
|
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
73
|
-
def self.
|
74
|
-
if Profiler.current_profile &&
|
75
|
-
Profiler.current_profile.
|
73
|
+
def self.profile_node_render(node)
|
74
|
+
if Profiler.current_profile && node.respond_to?(:render)
|
75
|
+
Profiler.current_profile.start_node(node)
|
76
76
|
output = yield
|
77
|
-
Profiler.current_profile.
|
77
|
+
Profiler.current_profile.end_node(node)
|
78
78
|
output
|
79
79
|
else
|
80
80
|
yield
|
@@ -132,11 +132,11 @@ module Liquid
|
|
132
132
|
@root_timing.children.length
|
133
133
|
end
|
134
134
|
|
135
|
-
def
|
136
|
-
@timing_stack.push(Timing.start(
|
135
|
+
def start_node(node)
|
136
|
+
@timing_stack.push(Timing.start(node, current_partial))
|
137
137
|
end
|
138
138
|
|
139
|
-
def
|
139
|
+
def end_node(_node)
|
140
140
|
timing = @timing_stack.pop
|
141
141
|
timing.finish
|
142
142
|
|
@@ -154,6 +154,5 @@ module Liquid
|
|
154
154
|
def pop_partial
|
155
155
|
@partial_stack.pop
|
156
156
|
end
|
157
|
-
|
158
157
|
end
|
159
158
|
end
|
data/lib/liquid/range_lookup.rb
CHANGED
@@ -16,7 +16,22 @@ module Liquid
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def evaluate(context)
|
19
|
-
context.evaluate(@start_obj)
|
19
|
+
start_int = to_integer(context.evaluate(@start_obj))
|
20
|
+
end_int = to_integer(context.evaluate(@end_obj))
|
21
|
+
start_int..end_int
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def to_integer(input)
|
27
|
+
case input
|
28
|
+
when Integer
|
29
|
+
input
|
30
|
+
when NilClass, String
|
31
|
+
input.to_i
|
32
|
+
else
|
33
|
+
Utils.to_integer(input)
|
34
|
+
end
|
20
35
|
end
|
21
36
|
end
|
22
37
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Liquid
|
2
|
+
class ResourceLimits
|
3
|
+
attr_accessor :render_length, :render_score, :assign_score,
|
4
|
+
:render_length_limit, :render_score_limit, :assign_score_limit
|
5
|
+
|
6
|
+
def initialize(limits)
|
7
|
+
@render_length_limit = limits[:render_length_limit]
|
8
|
+
@render_score_limit = limits[:render_score_limit]
|
9
|
+
@assign_score_limit = limits[:assign_score_limit]
|
10
|
+
reset
|
11
|
+
end
|
12
|
+
|
13
|
+
def reached?
|
14
|
+
(@render_length_limit && @render_length > @render_length_limit) ||
|
15
|
+
(@render_score_limit && @render_score > @render_score_limit) ||
|
16
|
+
(@assign_score_limit && @assign_score > @assign_score_limit)
|
17
|
+
end
|
18
|
+
|
19
|
+
def reset
|
20
|
+
@render_length = @render_score = @assign_score = 0
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -2,7 +2,6 @@ require 'cgi'
|
|
2
2
|
require 'bigdecimal'
|
3
3
|
|
4
4
|
module Liquid
|
5
|
-
|
6
5
|
module StandardFilters
|
7
6
|
HTML_ESCAPE = {
|
8
7
|
'&'.freeze => '&'.freeze,
|
@@ -34,7 +33,7 @@ module Liquid
|
|
34
33
|
end
|
35
34
|
|
36
35
|
def escape(input)
|
37
|
-
CGI.escapeHTML(input).untaint
|
36
|
+
CGI.escapeHTML(input).untaint unless input.nil?
|
38
37
|
end
|
39
38
|
alias_method :h, :escape
|
40
39
|
|
@@ -43,12 +42,16 @@ module Liquid
|
|
43
42
|
end
|
44
43
|
|
45
44
|
def url_encode(input)
|
46
|
-
CGI.escape(input)
|
45
|
+
CGI.escape(input) unless input.nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
def url_decode(input)
|
49
|
+
CGI.unescape(input) unless input.nil?
|
47
50
|
end
|
48
51
|
|
49
|
-
def slice(input, offset, length=nil)
|
50
|
-
offset =
|
51
|
-
length = length ?
|
52
|
+
def slice(input, offset, length = nil)
|
53
|
+
offset = Utils.to_integer(offset)
|
54
|
+
length = length ? Utils.to_integer(length) : 1
|
52
55
|
|
53
56
|
if input.is_a?(Array)
|
54
57
|
input.slice(offset, length) || []
|
@@ -59,18 +62,22 @@ module Liquid
|
|
59
62
|
|
60
63
|
# Truncate a string down to x characters
|
61
64
|
def truncate(input, length = 50, truncate_string = "...".freeze)
|
62
|
-
if input.nil?
|
63
|
-
|
65
|
+
return if input.nil?
|
66
|
+
input_str = input.to_s
|
67
|
+
length = Utils.to_integer(length)
|
68
|
+
truncate_string_str = truncate_string.to_s
|
69
|
+
l = length - truncate_string_str.length
|
64
70
|
l = 0 if l < 0
|
65
|
-
|
71
|
+
input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
|
66
72
|
end
|
67
73
|
|
68
74
|
def truncatewords(input, words = 15, truncate_string = "...".freeze)
|
69
|
-
if input.nil?
|
75
|
+
return if input.nil?
|
70
76
|
wordlist = input.to_s.split
|
71
|
-
|
77
|
+
words = Utils.to_integer(words)
|
78
|
+
l = words - 1
|
72
79
|
l = 0 if l < 0
|
73
|
-
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
|
80
|
+
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input
|
74
81
|
end
|
75
82
|
|
76
83
|
# Split input string into an array of substrings separated by given pattern.
|
@@ -79,7 +86,7 @@ module Liquid
|
|
79
86
|
# <div class="summary">{{ post | split '//' | first }}</div>
|
80
87
|
#
|
81
88
|
def split(input, pattern)
|
82
|
-
input.to_s.split(pattern)
|
89
|
+
input.to_s.split(pattern.to_s)
|
83
90
|
end
|
84
91
|
|
85
92
|
def strip(input)
|
@@ -115,10 +122,32 @@ module Liquid
|
|
115
122
|
ary = InputIterator.new(input)
|
116
123
|
if property.nil?
|
117
124
|
ary.sort
|
125
|
+
elsif ary.empty? # The next two cases assume a non-empty array.
|
126
|
+
[]
|
118
127
|
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
|
119
|
-
ary.sort
|
120
|
-
|
121
|
-
|
128
|
+
ary.sort do |a, b|
|
129
|
+
a = a[property]
|
130
|
+
b = b[property]
|
131
|
+
if a && b
|
132
|
+
a <=> b
|
133
|
+
else
|
134
|
+
a ? -1 : 1
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Sort elements of an array ignoring case if strings
|
141
|
+
# provide optional property with which to sort an array of hashes or drops
|
142
|
+
def sort_natural(input, property = nil)
|
143
|
+
ary = InputIterator.new(input)
|
144
|
+
|
145
|
+
if property.nil?
|
146
|
+
ary.sort { |a, b| a.casecmp(b) }
|
147
|
+
elsif ary.empty? # The next two cases assume a non-empty array.
|
148
|
+
[]
|
149
|
+
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
|
150
|
+
ary.sort { |a, b| a[property].casecmp(b[property]) }
|
122
151
|
end
|
123
152
|
end
|
124
153
|
|
@@ -126,10 +155,13 @@ module Liquid
|
|
126
155
|
# provide optional property with which to determine uniqueness
|
127
156
|
def uniq(input, property = nil)
|
128
157
|
ary = InputIterator.new(input)
|
158
|
+
|
129
159
|
if property.nil?
|
130
|
-
|
131
|
-
elsif
|
132
|
-
|
160
|
+
ary.uniq
|
161
|
+
elsif ary.empty? # The next two cases assume a non-empty array.
|
162
|
+
[]
|
163
|
+
elsif ary.first.respond_to?(:[])
|
164
|
+
ary.uniq{ |a| a[property] }
|
133
165
|
end
|
134
166
|
end
|
135
167
|
|
@@ -147,29 +179,44 @@ module Liquid
|
|
147
179
|
if property == "to_liquid".freeze
|
148
180
|
e
|
149
181
|
elsif e.respond_to?(:[])
|
150
|
-
e[property]
|
182
|
+
r = e[property]
|
183
|
+
r.is_a?(Proc) ? r.call : r
|
151
184
|
end
|
152
185
|
end
|
153
186
|
end
|
154
187
|
|
188
|
+
# Remove nils within an array
|
189
|
+
# provide optional property with which to check for nil
|
190
|
+
def compact(input, property = nil)
|
191
|
+
ary = InputIterator.new(input)
|
192
|
+
|
193
|
+
if property.nil?
|
194
|
+
ary.compact
|
195
|
+
elsif ary.empty? # The next two cases assume a non-empty array.
|
196
|
+
[]
|
197
|
+
elsif ary.first.respond_to?(:[])
|
198
|
+
ary.reject{ |a| a[property].nil? }
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
155
202
|
# Replace occurrences of a string with another
|
156
203
|
def replace(input, string, replacement = ''.freeze)
|
157
|
-
input.to_s.gsub(string, replacement.to_s)
|
204
|
+
input.to_s.gsub(string.to_s, replacement.to_s)
|
158
205
|
end
|
159
206
|
|
160
207
|
# Replace the first occurrences of a string with another
|
161
208
|
def replace_first(input, string, replacement = ''.freeze)
|
162
|
-
input.to_s.sub(string, replacement.to_s)
|
209
|
+
input.to_s.sub(string.to_s, replacement.to_s)
|
163
210
|
end
|
164
211
|
|
165
212
|
# remove a substring
|
166
213
|
def remove(input, string)
|
167
|
-
input.to_s.gsub(string, ''.freeze)
|
214
|
+
input.to_s.gsub(string.to_s, ''.freeze)
|
168
215
|
end
|
169
216
|
|
170
217
|
# remove the first occurrences of a substring
|
171
218
|
def remove_first(input, string)
|
172
|
-
input.to_s.sub(string, ''.freeze)
|
219
|
+
input.to_s.sub(string.to_s, ''.freeze)
|
173
220
|
end
|
174
221
|
|
175
222
|
# add one string to another
|
@@ -177,6 +224,13 @@ module Liquid
|
|
177
224
|
input.to_s + string.to_s
|
178
225
|
end
|
179
226
|
|
227
|
+
def concat(input, array)
|
228
|
+
unless array.respond_to?(:to_ary)
|
229
|
+
raise ArgumentError.new("concat filter requires an array argument")
|
230
|
+
end
|
231
|
+
InputIterator.new(input).concat(array)
|
232
|
+
end
|
233
|
+
|
180
234
|
# prepend a string to another
|
181
235
|
def prepend(input, string)
|
182
236
|
string.to_s + input.to_s
|
@@ -221,7 +275,7 @@ module Liquid
|
|
221
275
|
def date(input, format)
|
222
276
|
return input if format.to_s.empty?
|
223
277
|
|
224
|
-
return input unless date = to_date(input)
|
278
|
+
return input unless date = Utils.to_date(input)
|
225
279
|
|
226
280
|
date.strftime(format.to_s)
|
227
281
|
end
|
@@ -244,6 +298,12 @@ module Liquid
|
|
244
298
|
array.last if array.respond_to?(:last)
|
245
299
|
end
|
246
300
|
|
301
|
+
# absolute value
|
302
|
+
def abs(input)
|
303
|
+
result = Utils.to_number(input).abs
|
304
|
+
result.is_a?(BigDecimal) ? result.to_f : result
|
305
|
+
end
|
306
|
+
|
247
307
|
# addition
|
248
308
|
def plus(input, operand)
|
249
309
|
apply_operation(input, operand, :+)
|
@@ -262,66 +322,49 @@ module Liquid
|
|
262
322
|
# division
|
263
323
|
def divided_by(input, operand)
|
264
324
|
apply_operation(input, operand, :/)
|
325
|
+
rescue ::ZeroDivisionError => e
|
326
|
+
raise Liquid::ZeroDivisionError, e.message
|
265
327
|
end
|
266
328
|
|
267
329
|
def modulo(input, operand)
|
268
330
|
apply_operation(input, operand, :%)
|
331
|
+
rescue ::ZeroDivisionError => e
|
332
|
+
raise Liquid::ZeroDivisionError, e.message
|
269
333
|
end
|
270
334
|
|
271
335
|
def round(input, n = 0)
|
272
|
-
result = to_number(input).round(to_number(n))
|
336
|
+
result = Utils.to_number(input).round(Utils.to_number(n))
|
273
337
|
result = result.to_f if result.is_a?(BigDecimal)
|
274
338
|
result = result.to_i if n == 0
|
275
339
|
result
|
340
|
+
rescue ::FloatDomainError => e
|
341
|
+
raise Liquid::FloatDomainError, e.message
|
276
342
|
end
|
277
343
|
|
278
344
|
def ceil(input)
|
279
|
-
to_number(input).ceil.to_i
|
345
|
+
Utils.to_number(input).ceil.to_i
|
346
|
+
rescue ::FloatDomainError => e
|
347
|
+
raise Liquid::FloatDomainError, e.message
|
280
348
|
end
|
281
349
|
|
282
350
|
def floor(input)
|
283
|
-
to_number(input).floor.to_i
|
284
|
-
|
285
|
-
|
286
|
-
def default(input, default_value = "".freeze)
|
287
|
-
is_blank = input.respond_to?(:empty?) ? input.empty? : !input
|
288
|
-
is_blank ? default_value : input
|
351
|
+
Utils.to_number(input).floor.to_i
|
352
|
+
rescue ::FloatDomainError => e
|
353
|
+
raise Liquid::FloatDomainError, e.message
|
289
354
|
end
|
290
355
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
case obj
|
295
|
-
when Float
|
296
|
-
BigDecimal.new(obj.to_s)
|
297
|
-
when Numeric
|
298
|
-
obj
|
299
|
-
when String
|
300
|
-
(obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
|
356
|
+
def default(input, default_value = ''.freeze)
|
357
|
+
if !input || input.respond_to?(:empty?) && input.empty?
|
358
|
+
default_value
|
301
359
|
else
|
302
|
-
|
360
|
+
input
|
303
361
|
end
|
304
362
|
end
|
305
363
|
|
306
|
-
|
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
|
364
|
+
private
|
322
365
|
|
323
366
|
def apply_operation(input, operand, operation)
|
324
|
-
result = to_number(input).send(operation, to_number(operand))
|
367
|
+
result = Utils.to_number(input).send(operation, Utils.to_number(operand))
|
325
368
|
result.is_a?(BigDecimal) ? result.to_f : result
|
326
369
|
end
|
327
370
|
|
@@ -344,10 +387,27 @@ module Liquid
|
|
344
387
|
to_a.join(glue)
|
345
388
|
end
|
346
389
|
|
390
|
+
def concat(args)
|
391
|
+
to_a.concat(args)
|
392
|
+
end
|
393
|
+
|
347
394
|
def reverse
|
348
395
|
reverse_each.to_a
|
349
396
|
end
|
350
397
|
|
398
|
+
def uniq(&block)
|
399
|
+
to_a.uniq(&block)
|
400
|
+
end
|
401
|
+
|
402
|
+
def compact
|
403
|
+
to_a.compact
|
404
|
+
end
|
405
|
+
|
406
|
+
def empty?
|
407
|
+
@input.each { return false }
|
408
|
+
true
|
409
|
+
end
|
410
|
+
|
351
411
|
def each
|
352
412
|
@input.each do |e|
|
353
413
|
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
|