liquid 4.0.2 → 5.1.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 +59 -0
- data/README.md +6 -0
- data/lib/liquid/block.rb +31 -14
- data/lib/liquid/block_body.rb +166 -53
- data/lib/liquid/condition.rb +41 -20
- data/lib/liquid/context.rb +107 -52
- data/lib/liquid/document.rb +47 -9
- data/lib/liquid/drop.rb +4 -2
- data/lib/liquid/errors.rb +20 -18
- data/lib/liquid/expression.rb +29 -34
- data/lib/liquid/extensions.rb +2 -0
- data/lib/liquid/file_system.rb +6 -4
- data/lib/liquid/forloop_drop.rb +11 -4
- data/lib/liquid/i18n.rb +5 -3
- data/lib/liquid/interrupts.rb +3 -1
- data/lib/liquid/lexer.rb +30 -23
- data/lib/liquid/locales/en.yml +3 -1
- data/lib/liquid/parse_context.rb +20 -4
- data/lib/liquid/parse_tree_visitor.rb +2 -2
- data/lib/liquid/parser.rb +30 -18
- data/lib/liquid/parser_switching.rb +17 -3
- data/lib/liquid/partial_cache.rb +24 -0
- data/lib/liquid/profiler/hooks.rb +26 -14
- data/lib/liquid/profiler.rb +67 -86
- data/lib/liquid/range_lookup.rb +13 -3
- data/lib/liquid/register.rb +6 -0
- data/lib/liquid/resource_limits.rb +47 -8
- data/lib/liquid/standardfilters.rb +95 -46
- data/lib/liquid/static_registers.rb +44 -0
- data/lib/liquid/strainer_factory.rb +36 -0
- data/lib/liquid/strainer_template.rb +53 -0
- data/lib/liquid/tablerowloop_drop.rb +6 -4
- data/lib/liquid/tag/disableable.rb +22 -0
- data/lib/liquid/tag/disabler.rb +21 -0
- data/lib/liquid/tag.rb +28 -6
- data/lib/liquid/tags/assign.rb +24 -10
- data/lib/liquid/tags/break.rb +8 -3
- data/lib/liquid/tags/capture.rb +11 -8
- data/lib/liquid/tags/case.rb +40 -27
- data/lib/liquid/tags/comment.rb +5 -3
- data/lib/liquid/tags/continue.rb +8 -3
- data/lib/liquid/tags/cycle.rb +25 -14
- data/lib/liquid/tags/decrement.rb +6 -3
- data/lib/liquid/tags/echo.rb +34 -0
- data/lib/liquid/tags/for.rb +68 -44
- data/lib/liquid/tags/if.rb +39 -23
- data/lib/liquid/tags/ifchanged.rb +11 -10
- data/lib/liquid/tags/include.rb +34 -47
- data/lib/liquid/tags/increment.rb +7 -3
- data/lib/liquid/tags/raw.rb +14 -11
- data/lib/liquid/tags/render.rb +84 -0
- data/lib/liquid/tags/table_row.rb +23 -19
- data/lib/liquid/tags/unless.rb +23 -15
- data/lib/liquid/template.rb +53 -72
- data/lib/liquid/template_factory.rb +9 -0
- data/lib/liquid/tokenizer.rb +18 -10
- data/lib/liquid/usage.rb +8 -0
- data/lib/liquid/utils.rb +13 -3
- data/lib/liquid/variable.rb +46 -41
- data/lib/liquid/variable_lookup.rb +11 -6
- data/lib/liquid/version.rb +2 -1
- data/lib/liquid.rb +17 -5
- data/test/integration/assign_test.rb +74 -5
- data/test/integration/blank_test.rb +11 -8
- data/test/integration/block_test.rb +47 -1
- data/test/integration/capture_test.rb +18 -10
- data/test/integration/context_test.rb +609 -5
- data/test/integration/document_test.rb +4 -2
- data/test/integration/drop_test.rb +67 -83
- data/test/integration/error_handling_test.rb +73 -61
- data/test/integration/expression_test.rb +46 -0
- data/test/integration/filter_test.rb +53 -42
- data/test/integration/hash_ordering_test.rb +5 -3
- data/test/integration/output_test.rb +26 -24
- data/test/integration/parsing_quirks_test.rb +19 -7
- data/test/integration/{render_profiling_test.rb → profiler_test.rb} +84 -25
- data/test/integration/security_test.rb +30 -21
- data/test/integration/standard_filter_test.rb +385 -281
- data/test/integration/tag/disableable_test.rb +59 -0
- data/test/integration/tag_test.rb +45 -0
- data/test/integration/tags/break_tag_test.rb +4 -2
- data/test/integration/tags/continue_tag_test.rb +4 -2
- data/test/integration/tags/echo_test.rb +13 -0
- data/test/integration/tags/for_tag_test.rb +107 -51
- data/test/integration/tags/if_else_tag_test.rb +5 -3
- data/test/integration/tags/include_tag_test.rb +76 -52
- data/test/integration/tags/increment_tag_test.rb +4 -2
- data/test/integration/tags/liquid_tag_test.rb +116 -0
- data/test/integration/tags/raw_tag_test.rb +14 -11
- data/test/integration/tags/render_tag_test.rb +213 -0
- data/test/integration/tags/standard_tag_test.rb +38 -31
- data/test/integration/tags/statements_test.rb +23 -21
- data/test/integration/tags/table_row_test.rb +2 -0
- data/test/integration/tags/unless_else_tag_test.rb +4 -2
- data/test/integration/template_test.rb +132 -124
- data/test/integration/trim_mode_test.rb +78 -44
- data/test/integration/variable_test.rb +74 -32
- data/test/test_helper.rb +113 -22
- data/test/unit/block_unit_test.rb +19 -24
- data/test/unit/condition_unit_test.rb +79 -77
- data/test/unit/file_system_unit_test.rb +6 -4
- data/test/unit/i18n_unit_test.rb +7 -5
- data/test/unit/lexer_unit_test.rb +11 -9
- data/test/{integration → unit}/parse_tree_visitor_test.rb +16 -2
- data/test/unit/parser_unit_test.rb +37 -35
- data/test/unit/partial_cache_unit_test.rb +128 -0
- data/test/unit/regexp_unit_test.rb +17 -15
- data/test/unit/static_registers_unit_test.rb +156 -0
- data/test/unit/strainer_factory_unit_test.rb +100 -0
- data/test/unit/strainer_template_unit_test.rb +82 -0
- data/test/unit/tag_unit_test.rb +5 -3
- data/test/unit/tags/case_tag_unit_test.rb +3 -1
- data/test/unit/tags/for_tag_unit_test.rb +4 -2
- data/test/unit/tags/if_tag_unit_test.rb +3 -1
- data/test/unit/template_factory_unit_test.rb +12 -0
- data/test/unit/template_unit_test.rb +19 -10
- data/test/unit/tokenizer_unit_test.rb +26 -19
- data/test/unit/variable_unit_test.rb +51 -49
- metadata +76 -50
- data/lib/liquid/strainer.rb +0 -66
- data/lib/liquid/truffle.rb +0 -5
- data/test/truffle/truffle_test.rb +0 -9
- data/test/unit/context_unit_test.rb +0 -489
- data/test/unit/strainer_unit_test.rb +0 -164
@@ -1,15 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Liquid
|
2
4
|
module ParserSwitching
|
5
|
+
def strict_parse_with_error_mode_fallback(markup)
|
6
|
+
strict_parse_with_error_context(markup)
|
7
|
+
rescue SyntaxError => e
|
8
|
+
case parse_context.error_mode
|
9
|
+
when :strict
|
10
|
+
raise
|
11
|
+
when :warn
|
12
|
+
parse_context.warnings << e
|
13
|
+
end
|
14
|
+
lax_parse(markup)
|
15
|
+
end
|
16
|
+
|
3
17
|
def parse_with_selected_parser(markup)
|
4
18
|
case parse_context.error_mode
|
5
19
|
when :strict then strict_parse_with_error_context(markup)
|
6
20
|
when :lax then lax_parse(markup)
|
7
21
|
when :warn
|
8
22
|
begin
|
9
|
-
|
23
|
+
strict_parse_with_error_context(markup)
|
10
24
|
rescue SyntaxError => e
|
11
25
|
parse_context.warnings << e
|
12
|
-
|
26
|
+
lax_parse(markup)
|
13
27
|
end
|
14
28
|
end
|
15
29
|
end
|
@@ -19,7 +33,7 @@ module Liquid
|
|
19
33
|
def strict_parse_with_error_context(markup)
|
20
34
|
strict_parse(markup)
|
21
35
|
rescue SyntaxError => e
|
22
|
-
e.line_number
|
36
|
+
e.line_number = line_number
|
23
37
|
e.markup_context = markup_context(markup)
|
24
38
|
raise e
|
25
39
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid
|
4
|
+
class PartialCache
|
5
|
+
def self.load(template_name, context:, parse_context:)
|
6
|
+
cached_partials = (context.registers[:cached_partials] ||= {})
|
7
|
+
cached = cached_partials[template_name]
|
8
|
+
return cached if cached
|
9
|
+
|
10
|
+
file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
|
11
|
+
source = file_system.read_template_file(template_name)
|
12
|
+
|
13
|
+
parse_context.partial = true
|
14
|
+
|
15
|
+
template_factory = (context.registers[:template_factory] ||= Liquid::TemplateFactory.new)
|
16
|
+
template = template_factory.for(template_name)
|
17
|
+
|
18
|
+
partial = template.parse(source, parse_context)
|
19
|
+
cached_partials[template_name] = partial
|
20
|
+
ensure
|
21
|
+
parse_context.partial = false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,23 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Liquid
|
2
|
-
|
3
|
-
def
|
4
|
-
|
5
|
-
|
4
|
+
module BlockBodyProfilingHook
|
5
|
+
def render_node(context, output, node)
|
6
|
+
if (profiler = context.profiler)
|
7
|
+
profiler.profile_node(context.template_name, code: node.raw, line_number: node.line_number) do
|
8
|
+
super
|
9
|
+
end
|
10
|
+
else
|
11
|
+
super
|
6
12
|
end
|
7
13
|
end
|
8
|
-
|
9
|
-
alias_method :render_node_without_profiling, :render_node_to_output
|
10
|
-
alias_method :render_node_to_output, :render_node_with_profiling
|
11
14
|
end
|
15
|
+
BlockBody.prepend(BlockBodyProfilingHook)
|
12
16
|
|
13
|
-
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
-
end
|
17
|
+
module DocumentProfilingHook
|
18
|
+
def render_to_output_buffer(context, output)
|
19
|
+
return super unless context.profiler
|
20
|
+
context.profiler.profile(context.template_name) { super }
|
18
21
|
end
|
22
|
+
end
|
23
|
+
Document.prepend(DocumentProfilingHook)
|
24
|
+
|
25
|
+
module ContextProfilingHook
|
26
|
+
attr_accessor :profiler
|
19
27
|
|
20
|
-
|
21
|
-
|
28
|
+
def new_isolated_subcontext
|
29
|
+
new_context = super
|
30
|
+
new_context.profiler = profiler
|
31
|
+
new_context
|
32
|
+
end
|
22
33
|
end
|
34
|
+
Context.prepend(ContextProfilingHook)
|
23
35
|
end
|
data/lib/liquid/profiler.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'liquid/profiler/hooks'
|
2
4
|
|
3
5
|
module Liquid
|
@@ -23,7 +25,7 @@ module Liquid
|
|
23
25
|
# node.code
|
24
26
|
#
|
25
27
|
# # Which template and line number of this node.
|
26
|
-
# #
|
28
|
+
# # The top-level template name is `nil` by default, but can be set in the Liquid::Context before rendering.
|
27
29
|
# node.partial
|
28
30
|
# node.line_number
|
29
31
|
#
|
@@ -44,115 +46,94 @@ module Liquid
|
|
44
46
|
include Enumerable
|
45
47
|
|
46
48
|
class Timing
|
47
|
-
attr_reader :code, :
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
@
|
49
|
+
attr_reader :code, :template_name, :line_number, :children
|
50
|
+
attr_accessor :total_time
|
51
|
+
alias_method :render_time, :total_time
|
52
|
+
alias_method :partial, :template_name
|
53
|
+
|
54
|
+
def initialize(code: nil, template_name: nil, line_number: nil)
|
55
|
+
@code = code
|
56
|
+
@template_name = template_name
|
57
|
+
@line_number = line_number
|
58
|
+
@children = []
|
54
59
|
end
|
55
60
|
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
def finish
|
65
|
-
@end_time = Time.now
|
66
|
-
end
|
67
|
-
|
68
|
-
def render_time
|
69
|
-
@end_time - @start_time
|
61
|
+
def self_time
|
62
|
+
@self_time ||= begin
|
63
|
+
total_children_time = 0.0
|
64
|
+
@children.each do |child|
|
65
|
+
total_children_time += child.total_time
|
66
|
+
end
|
67
|
+
@total_time - total_children_time
|
68
|
+
end
|
70
69
|
end
|
71
70
|
end
|
72
71
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
72
|
+
attr_reader :total_time
|
73
|
+
alias_method :total_render_time, :total_time
|
74
|
+
|
75
|
+
def initialize
|
76
|
+
@root_children = []
|
77
|
+
@current_children = nil
|
78
|
+
@total_time = 0.0
|
79
|
+
end
|
80
|
+
|
81
|
+
def profile(template_name, &block)
|
82
|
+
# nested renders are done from a tag that already has a timing node
|
83
|
+
return yield if @current_children
|
84
|
+
|
85
|
+
root_children = @root_children
|
86
|
+
render_idx = root_children.length
|
87
|
+
begin
|
88
|
+
@current_children = root_children
|
89
|
+
profile_node(template_name, &block)
|
90
|
+
ensure
|
91
|
+
@current_children = nil
|
92
|
+
if (timing = root_children[render_idx])
|
93
|
+
@total_time += timing.total_time
|
94
|
+
end
|
81
95
|
end
|
82
96
|
end
|
83
97
|
|
84
|
-
def
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
Profiler.current_profile.pop_partial
|
89
|
-
output
|
98
|
+
def children
|
99
|
+
children = @root_children
|
100
|
+
if children.length == 1
|
101
|
+
children.first.children
|
90
102
|
else
|
91
|
-
|
103
|
+
children
|
92
104
|
end
|
93
105
|
end
|
94
106
|
|
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
107
|
def each(&block)
|
124
|
-
|
108
|
+
children.each(&block)
|
125
109
|
end
|
126
110
|
|
127
111
|
def [](idx)
|
128
|
-
|
112
|
+
children[idx]
|
129
113
|
end
|
130
114
|
|
131
115
|
def length
|
132
|
-
|
116
|
+
children.length
|
133
117
|
end
|
134
118
|
|
135
|
-
def
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
@partial_stack.last
|
119
|
+
def profile_node(template_name, code: nil, line_number: nil)
|
120
|
+
timing = Timing.new(code: code, template_name: template_name, line_number: line_number)
|
121
|
+
parent_children = @current_children
|
122
|
+
start_time = monotonic_time
|
123
|
+
begin
|
124
|
+
@current_children = timing.children
|
125
|
+
yield
|
126
|
+
ensure
|
127
|
+
@current_children = parent_children
|
128
|
+
timing.total_time = monotonic_time - start_time
|
129
|
+
parent_children << timing
|
130
|
+
end
|
148
131
|
end
|
149
132
|
|
150
|
-
|
151
|
-
@partial_stack.push(partial_name)
|
152
|
-
end
|
133
|
+
private
|
153
134
|
|
154
|
-
def
|
155
|
-
|
135
|
+
def monotonic_time
|
136
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
156
137
|
end
|
157
138
|
end
|
158
139
|
end
|
data/lib/liquid/range_lookup.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Liquid
|
2
4
|
class RangeLookup
|
3
5
|
def self.parse(start_markup, end_markup)
|
4
6
|
start_obj = Expression.parse(start_markup)
|
5
|
-
end_obj
|
7
|
+
end_obj = Expression.parse(end_markup)
|
6
8
|
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
|
7
9
|
new(start_obj, end_obj)
|
8
10
|
else
|
@@ -10,14 +12,16 @@ module Liquid
|
|
10
12
|
end
|
11
13
|
end
|
12
14
|
|
15
|
+
attr_reader :start_obj, :end_obj
|
16
|
+
|
13
17
|
def initialize(start_obj, end_obj)
|
14
18
|
@start_obj = start_obj
|
15
|
-
@end_obj
|
19
|
+
@end_obj = end_obj
|
16
20
|
end
|
17
21
|
|
18
22
|
def evaluate(context)
|
19
23
|
start_int = to_integer(context.evaluate(@start_obj))
|
20
|
-
end_int
|
24
|
+
end_int = to_integer(context.evaluate(@end_obj))
|
21
25
|
start_int..end_int
|
22
26
|
end
|
23
27
|
|
@@ -33,5 +37,11 @@ module Liquid
|
|
33
37
|
Utils.to_integer(input)
|
34
38
|
end
|
35
39
|
end
|
40
|
+
|
41
|
+
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
42
|
+
def children
|
43
|
+
[@node.start_obj, @node.end_obj]
|
44
|
+
end
|
45
|
+
end
|
36
46
|
end
|
37
47
|
end
|
@@ -1,23 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Liquid
|
2
4
|
class ResourceLimits
|
3
|
-
attr_accessor :
|
4
|
-
|
5
|
+
attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit
|
6
|
+
attr_reader :render_score, :assign_score
|
5
7
|
|
6
8
|
def initialize(limits)
|
7
9
|
@render_length_limit = limits[:render_length_limit]
|
8
|
-
@render_score_limit
|
9
|
-
@assign_score_limit
|
10
|
+
@render_score_limit = limits[:render_score_limit]
|
11
|
+
@assign_score_limit = limits[:assign_score_limit]
|
10
12
|
reset
|
11
13
|
end
|
12
14
|
|
15
|
+
def increment_render_score(amount)
|
16
|
+
@render_score += amount
|
17
|
+
raise_limits_reached if @render_score_limit && @render_score > @render_score_limit
|
18
|
+
end
|
19
|
+
|
20
|
+
def increment_assign_score(amount)
|
21
|
+
@assign_score += amount
|
22
|
+
raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit
|
23
|
+
end
|
24
|
+
|
25
|
+
# update either render_length or assign_score based on whether or not the writes are captured
|
26
|
+
def increment_write_score(output)
|
27
|
+
if (last_captured = @last_capture_length)
|
28
|
+
captured = output.bytesize
|
29
|
+
increment = captured - last_captured
|
30
|
+
@last_capture_length = captured
|
31
|
+
increment_assign_score(increment)
|
32
|
+
elsif @render_length_limit && output.bytesize > @render_length_limit
|
33
|
+
raise_limits_reached
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def raise_limits_reached
|
38
|
+
@reached_limit = true
|
39
|
+
raise MemoryError, "Memory limits exceeded"
|
40
|
+
end
|
41
|
+
|
13
42
|
def reached?
|
14
|
-
|
15
|
-
(@render_score_limit && @render_score > @render_score_limit) ||
|
16
|
-
(@assign_score_limit && @assign_score > @assign_score_limit)
|
43
|
+
@reached_limit
|
17
44
|
end
|
18
45
|
|
19
46
|
def reset
|
20
|
-
@
|
47
|
+
@reached_limit = false
|
48
|
+
@last_capture_length = nil
|
49
|
+
@render_score = @assign_score = 0
|
50
|
+
end
|
51
|
+
|
52
|
+
def with_capture
|
53
|
+
old_capture_length = @last_capture_length
|
54
|
+
begin
|
55
|
+
@last_capture_length = 0
|
56
|
+
yield
|
57
|
+
ensure
|
58
|
+
@last_capture_length = old_capture_length
|
59
|
+
end
|
21
60
|
end
|
22
61
|
end
|
23
62
|
end
|
@@ -1,20 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'cgi'
|
4
|
+
require 'base64'
|
2
5
|
require 'bigdecimal'
|
3
6
|
|
4
7
|
module Liquid
|
5
8
|
module StandardFilters
|
9
|
+
MAX_INT = (1 << 31) - 1
|
6
10
|
HTML_ESCAPE = {
|
7
|
-
'&'
|
8
|
-
'>'
|
9
|
-
'<'
|
10
|
-
'"'
|
11
|
-
"'"
|
11
|
+
'&' => '&',
|
12
|
+
'>' => '>',
|
13
|
+
'<' => '<',
|
14
|
+
'"' => '"',
|
15
|
+
"'" => ''',
|
12
16
|
}.freeze
|
13
17
|
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
|
14
|
-
STRIP_HTML_BLOCKS
|
15
|
-
|
18
|
+
STRIP_HTML_BLOCKS = Regexp.union(
|
19
|
+
%r{<script.*?</script>}m,
|
16
20
|
/<!--.*?-->/m,
|
17
|
-
|
21
|
+
%r{<style.*?</style>}m
|
18
22
|
)
|
19
23
|
STRIP_HTML_TAGS = /<.*?>/m
|
20
24
|
|
@@ -39,7 +43,7 @@ module Liquid
|
|
39
43
|
end
|
40
44
|
|
41
45
|
def escape(input)
|
42
|
-
CGI.escapeHTML(input.to_s)
|
46
|
+
CGI.escapeHTML(input.to_s) unless input.nil?
|
43
47
|
end
|
44
48
|
alias_method :h, :escape
|
45
49
|
|
@@ -60,6 +64,26 @@ module Liquid
|
|
60
64
|
result
|
61
65
|
end
|
62
66
|
|
67
|
+
def base64_encode(input)
|
68
|
+
Base64.strict_encode64(input.to_s)
|
69
|
+
end
|
70
|
+
|
71
|
+
def base64_decode(input)
|
72
|
+
Base64.strict_decode64(input.to_s)
|
73
|
+
rescue ::ArgumentError
|
74
|
+
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
|
75
|
+
end
|
76
|
+
|
77
|
+
def base64_url_safe_encode(input)
|
78
|
+
Base64.urlsafe_encode64(input.to_s)
|
79
|
+
end
|
80
|
+
|
81
|
+
def base64_url_safe_decode(input)
|
82
|
+
Base64.urlsafe_decode64(input.to_s)
|
83
|
+
rescue ::ArgumentError
|
84
|
+
raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
|
85
|
+
end
|
86
|
+
|
63
87
|
def slice(input, offset, length = nil)
|
64
88
|
offset = Utils.to_integer(offset)
|
65
89
|
length = length ? Utils.to_integer(length) : 1
|
@@ -72,23 +96,36 @@ module Liquid
|
|
72
96
|
end
|
73
97
|
|
74
98
|
# Truncate a string down to x characters
|
75
|
-
def truncate(input, length = 50, truncate_string = "..."
|
99
|
+
def truncate(input, length = 50, truncate_string = "...")
|
76
100
|
return if input.nil?
|
77
101
|
input_str = input.to_s
|
78
|
-
length
|
102
|
+
length = Utils.to_integer(length)
|
103
|
+
|
79
104
|
truncate_string_str = truncate_string.to_s
|
105
|
+
|
80
106
|
l = length - truncate_string_str.length
|
81
107
|
l = 0 if l < 0
|
82
|
-
|
108
|
+
|
109
|
+
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
|
83
110
|
end
|
84
111
|
|
85
|
-
def truncatewords(input, words = 15, truncate_string = "..."
|
112
|
+
def truncatewords(input, words = 15, truncate_string = "...")
|
86
113
|
return if input.nil?
|
87
|
-
|
114
|
+
input = input.to_s
|
88
115
|
words = Utils.to_integer(words)
|
89
|
-
|
90
|
-
|
91
|
-
wordlist
|
116
|
+
words = 1 if words <= 0
|
117
|
+
|
118
|
+
wordlist = begin
|
119
|
+
input.split(" ", words + 1)
|
120
|
+
rescue RangeError
|
121
|
+
raise if words + 1 < MAX_INT
|
122
|
+
# e.g. integer #{words} too big to convert to `int'
|
123
|
+
raise Liquid::ArgumentError, "integer #{words} too big for truncatewords"
|
124
|
+
end
|
125
|
+
return input if wordlist.length <= words
|
126
|
+
|
127
|
+
wordlist.pop
|
128
|
+
wordlist.join(" ").concat(truncate_string.to_s)
|
92
129
|
end
|
93
130
|
|
94
131
|
# Split input string into an array of substrings separated by given pattern.
|
@@ -113,7 +150,7 @@ module Liquid
|
|
113
150
|
end
|
114
151
|
|
115
152
|
def strip_html(input)
|
116
|
-
empty
|
153
|
+
empty = ''
|
117
154
|
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
|
118
155
|
result.gsub!(STRIP_HTML_TAGS, empty)
|
119
156
|
result
|
@@ -121,18 +158,18 @@ module Liquid
|
|
121
158
|
|
122
159
|
# Remove all newlines from the string
|
123
160
|
def strip_newlines(input)
|
124
|
-
input.to_s.gsub(/\r?\n/, ''
|
161
|
+
input.to_s.gsub(/\r?\n/, '')
|
125
162
|
end
|
126
163
|
|
127
164
|
# Join elements of the array with certain character between them
|
128
|
-
def join(input, glue = ' '
|
129
|
-
InputIterator.new(input).join(glue)
|
165
|
+
def join(input, glue = ' ')
|
166
|
+
InputIterator.new(input, context).join(glue)
|
130
167
|
end
|
131
168
|
|
132
169
|
# Sort elements of the array
|
133
170
|
# provide optional property with which to sort an array of hashes or drops
|
134
171
|
def sort(input, property = nil)
|
135
|
-
ary = InputIterator.new(input)
|
172
|
+
ary = InputIterator.new(input, context)
|
136
173
|
|
137
174
|
return [] if ary.empty?
|
138
175
|
|
@@ -152,7 +189,7 @@ module Liquid
|
|
152
189
|
# Sort elements of an array ignoring case if strings
|
153
190
|
# provide optional property with which to sort an array of hashes or drops
|
154
191
|
def sort_natural(input, property = nil)
|
155
|
-
ary = InputIterator.new(input)
|
192
|
+
ary = InputIterator.new(input, context)
|
156
193
|
|
157
194
|
return [] if ary.empty?
|
158
195
|
|
@@ -172,7 +209,7 @@ module Liquid
|
|
172
209
|
# Filter the elements of an array to those with a certain property value.
|
173
210
|
# By default the target is any truthy value.
|
174
211
|
def where(input, property, target_value = nil)
|
175
|
-
ary = InputIterator.new(input)
|
212
|
+
ary = InputIterator.new(input, context)
|
176
213
|
|
177
214
|
if ary.empty?
|
178
215
|
[]
|
@@ -194,7 +231,7 @@ module Liquid
|
|
194
231
|
# Remove duplicate elements from an array
|
195
232
|
# provide optional property with which to determine uniqueness
|
196
233
|
def uniq(input, property = nil)
|
197
|
-
ary = InputIterator.new(input)
|
234
|
+
ary = InputIterator.new(input, context)
|
198
235
|
|
199
236
|
if property.nil?
|
200
237
|
ary.uniq
|
@@ -211,16 +248,16 @@ module Liquid
|
|
211
248
|
|
212
249
|
# Reverse the elements of an array
|
213
250
|
def reverse(input)
|
214
|
-
ary = InputIterator.new(input)
|
251
|
+
ary = InputIterator.new(input, context)
|
215
252
|
ary.reverse
|
216
253
|
end
|
217
254
|
|
218
255
|
# map/collect on a given property
|
219
256
|
def map(input, property)
|
220
|
-
InputIterator.new(input).map do |e|
|
257
|
+
InputIterator.new(input, context).map do |e|
|
221
258
|
e = e.call if e.is_a?(Proc)
|
222
259
|
|
223
|
-
if property == "to_liquid"
|
260
|
+
if property == "to_liquid"
|
224
261
|
e
|
225
262
|
elsif e.respond_to?(:[])
|
226
263
|
r = e[property]
|
@@ -234,7 +271,7 @@ module Liquid
|
|
234
271
|
# Remove nils within an array
|
235
272
|
# provide optional property with which to check for nil
|
236
273
|
def compact(input, property = nil)
|
237
|
-
ary = InputIterator.new(input)
|
274
|
+
ary = InputIterator.new(input, context)
|
238
275
|
|
239
276
|
if property.nil?
|
240
277
|
ary.compact
|
@@ -250,23 +287,23 @@ module Liquid
|
|
250
287
|
end
|
251
288
|
|
252
289
|
# Replace occurrences of a string with another
|
253
|
-
def replace(input, string, replacement = ''
|
290
|
+
def replace(input, string, replacement = '')
|
254
291
|
input.to_s.gsub(string.to_s, replacement.to_s)
|
255
292
|
end
|
256
293
|
|
257
294
|
# Replace the first occurrences of a string with another
|
258
|
-
def replace_first(input, string, replacement = ''
|
295
|
+
def replace_first(input, string, replacement = '')
|
259
296
|
input.to_s.sub(string.to_s, replacement.to_s)
|
260
297
|
end
|
261
298
|
|
262
299
|
# remove a substring
|
263
300
|
def remove(input, string)
|
264
|
-
input.to_s.gsub(string.to_s, ''
|
301
|
+
input.to_s.gsub(string.to_s, '')
|
265
302
|
end
|
266
303
|
|
267
304
|
# remove the first occurrences of a substring
|
268
305
|
def remove_first(input, string)
|
269
|
-
input.to_s.sub(string.to_s, ''
|
306
|
+
input.to_s.sub(string.to_s, '')
|
270
307
|
end
|
271
308
|
|
272
309
|
# add one string to another
|
@@ -276,9 +313,9 @@ module Liquid
|
|
276
313
|
|
277
314
|
def concat(input, array)
|
278
315
|
unless array.respond_to?(:to_ary)
|
279
|
-
raise ArgumentError
|
316
|
+
raise ArgumentError, "concat filter requires an array argument"
|
280
317
|
end
|
281
|
-
InputIterator.new(input).concat(array)
|
318
|
+
InputIterator.new(input, context).concat(array)
|
282
319
|
end
|
283
320
|
|
284
321
|
# prepend a string to another
|
@@ -288,7 +325,7 @@ module Liquid
|
|
288
325
|
|
289
326
|
# Add <br /> tags in front of all newlines in input string
|
290
327
|
def newline_to_br(input)
|
291
|
-
input.to_s.gsub(/\n/, "<br />\n"
|
328
|
+
input.to_s.gsub(/\r?\n/, "<br />\n")
|
292
329
|
end
|
293
330
|
|
294
331
|
# Reformat a date using Ruby's core Time#strftime( string ) -> string
|
@@ -325,7 +362,7 @@ module Liquid
|
|
325
362
|
def date(input, format)
|
326
363
|
return input if format.to_s.empty?
|
327
364
|
|
328
|
-
return input unless date = Utils.to_date(input)
|
365
|
+
return input unless (date = Utils.to_date(input))
|
329
366
|
|
330
367
|
date.strftime(format.to_s)
|
331
368
|
end
|
@@ -419,18 +456,28 @@ module Liquid
|
|
419
456
|
result.is_a?(BigDecimal) ? result.to_f : result
|
420
457
|
end
|
421
458
|
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
459
|
+
# Set a default value when the input is nil, false or empty
|
460
|
+
#
|
461
|
+
# Example:
|
462
|
+
# {{ product.title | default: "No Title" }}
|
463
|
+
#
|
464
|
+
# Use `allow_false` when an input should only be tested against nil or empty and not false.
|
465
|
+
#
|
466
|
+
# Example:
|
467
|
+
# {{ product.title | default: "No Title", allow_false: true }}
|
468
|
+
#
|
469
|
+
def default(input, default_value = '', options = {})
|
470
|
+
options = {} unless options.is_a?(Hash)
|
471
|
+
false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input)
|
472
|
+
false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
|
428
473
|
end
|
429
474
|
|
430
475
|
private
|
431
476
|
|
477
|
+
attr_reader :context
|
478
|
+
|
432
479
|
def raise_property_error(property)
|
433
|
-
raise Liquid::ArgumentError
|
480
|
+
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
|
434
481
|
end
|
435
482
|
|
436
483
|
def apply_operation(input, operand, operation)
|
@@ -457,8 +504,9 @@ module Liquid
|
|
457
504
|
class InputIterator
|
458
505
|
include Enumerable
|
459
506
|
|
460
|
-
def initialize(input)
|
461
|
-
@
|
507
|
+
def initialize(input, context)
|
508
|
+
@context = context
|
509
|
+
@input = if input.is_a?(Array)
|
462
510
|
input.flatten
|
463
511
|
elsif input.is_a?(Hash)
|
464
512
|
[input]
|
@@ -496,6 +544,7 @@ module Liquid
|
|
496
544
|
|
497
545
|
def each
|
498
546
|
@input.each do |e|
|
547
|
+
e.context = @context if e.respond_to?(:context=)
|
499
548
|
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
|
500
549
|
end
|
501
550
|
end
|