liquid 4.0.3 → 5.0.1

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.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +43 -0
  3. data/README.md +6 -0
  4. data/lib/liquid.rb +17 -5
  5. data/lib/liquid/block.rb +31 -14
  6. data/lib/liquid/block_body.rb +166 -54
  7. data/lib/liquid/condition.rb +39 -18
  8. data/lib/liquid/context.rb +106 -51
  9. data/lib/liquid/document.rb +47 -9
  10. data/lib/liquid/drop.rb +4 -2
  11. data/lib/liquid/errors.rb +20 -18
  12. data/lib/liquid/expression.rb +29 -34
  13. data/lib/liquid/extensions.rb +2 -0
  14. data/lib/liquid/file_system.rb +6 -4
  15. data/lib/liquid/forloop_drop.rb +11 -4
  16. data/lib/liquid/i18n.rb +5 -3
  17. data/lib/liquid/interrupts.rb +3 -1
  18. data/lib/liquid/lexer.rb +30 -23
  19. data/lib/liquid/locales/en.yml +3 -1
  20. data/lib/liquid/parse_context.rb +20 -4
  21. data/lib/liquid/parse_tree_visitor.rb +2 -2
  22. data/lib/liquid/parser.rb +30 -18
  23. data/lib/liquid/parser_switching.rb +17 -3
  24. data/lib/liquid/partial_cache.rb +24 -0
  25. data/lib/liquid/profiler.rb +67 -86
  26. data/lib/liquid/profiler/hooks.rb +26 -14
  27. data/lib/liquid/range_lookup.rb +5 -3
  28. data/lib/liquid/register.rb +6 -0
  29. data/lib/liquid/resource_limits.rb +47 -8
  30. data/lib/liquid/standardfilters.rb +67 -46
  31. data/lib/liquid/static_registers.rb +44 -0
  32. data/lib/liquid/strainer_factory.rb +36 -0
  33. data/lib/liquid/strainer_template.rb +53 -0
  34. data/lib/liquid/tablerowloop_drop.rb +6 -4
  35. data/lib/liquid/tag.rb +28 -6
  36. data/lib/liquid/tag/disableable.rb +22 -0
  37. data/lib/liquid/tag/disabler.rb +21 -0
  38. data/lib/liquid/tags/assign.rb +24 -10
  39. data/lib/liquid/tags/break.rb +8 -3
  40. data/lib/liquid/tags/capture.rb +11 -8
  41. data/lib/liquid/tags/case.rb +33 -27
  42. data/lib/liquid/tags/comment.rb +5 -3
  43. data/lib/liquid/tags/continue.rb +8 -3
  44. data/lib/liquid/tags/cycle.rb +25 -14
  45. data/lib/liquid/tags/decrement.rb +6 -3
  46. data/lib/liquid/tags/echo.rb +34 -0
  47. data/lib/liquid/tags/for.rb +68 -44
  48. data/lib/liquid/tags/if.rb +35 -23
  49. data/lib/liquid/tags/ifchanged.rb +11 -10
  50. data/lib/liquid/tags/include.rb +34 -47
  51. data/lib/liquid/tags/increment.rb +7 -3
  52. data/lib/liquid/tags/raw.rb +14 -11
  53. data/lib/liquid/tags/render.rb +84 -0
  54. data/lib/liquid/tags/table_row.rb +23 -19
  55. data/lib/liquid/tags/unless.rb +15 -15
  56. data/lib/liquid/template.rb +53 -72
  57. data/lib/liquid/template_factory.rb +9 -0
  58. data/lib/liquid/tokenizer.rb +17 -9
  59. data/lib/liquid/usage.rb +8 -0
  60. data/lib/liquid/utils.rb +5 -3
  61. data/lib/liquid/variable.rb +46 -41
  62. data/lib/liquid/variable_lookup.rb +8 -6
  63. data/lib/liquid/version.rb +2 -1
  64. data/test/integration/assign_test.rb +74 -5
  65. data/test/integration/blank_test.rb +11 -8
  66. data/test/integration/block_test.rb +47 -1
  67. data/test/integration/capture_test.rb +18 -10
  68. data/test/integration/context_test.rb +609 -5
  69. data/test/integration/document_test.rb +4 -2
  70. data/test/integration/drop_test.rb +67 -83
  71. data/test/integration/error_handling_test.rb +73 -61
  72. data/test/integration/expression_test.rb +46 -0
  73. data/test/integration/filter_test.rb +53 -42
  74. data/test/integration/hash_ordering_test.rb +5 -3
  75. data/test/integration/output_test.rb +26 -24
  76. data/test/integration/parsing_quirks_test.rb +19 -7
  77. data/test/integration/{render_profiling_test.rb → profiler_test.rb} +84 -25
  78. data/test/integration/security_test.rb +30 -21
  79. data/test/integration/standard_filter_test.rb +343 -281
  80. data/test/integration/tag/disableable_test.rb +59 -0
  81. data/test/integration/tag_test.rb +45 -0
  82. data/test/integration/tags/break_tag_test.rb +4 -2
  83. data/test/integration/tags/continue_tag_test.rb +4 -2
  84. data/test/integration/tags/echo_test.rb +13 -0
  85. data/test/integration/tags/for_tag_test.rb +107 -51
  86. data/test/integration/tags/if_else_tag_test.rb +5 -3
  87. data/test/integration/tags/include_tag_test.rb +70 -54
  88. data/test/integration/tags/increment_tag_test.rb +4 -2
  89. data/test/integration/tags/liquid_tag_test.rb +116 -0
  90. data/test/integration/tags/raw_tag_test.rb +14 -11
  91. data/test/integration/tags/render_tag_test.rb +213 -0
  92. data/test/integration/tags/standard_tag_test.rb +38 -31
  93. data/test/integration/tags/statements_test.rb +23 -21
  94. data/test/integration/tags/table_row_test.rb +2 -0
  95. data/test/integration/tags/unless_else_tag_test.rb +4 -2
  96. data/test/integration/template_test.rb +118 -124
  97. data/test/integration/trim_mode_test.rb +78 -44
  98. data/test/integration/variable_test.rb +43 -32
  99. data/test/test_helper.rb +75 -22
  100. data/test/unit/block_unit_test.rb +19 -24
  101. data/test/unit/condition_unit_test.rb +79 -77
  102. data/test/unit/file_system_unit_test.rb +6 -4
  103. data/test/unit/i18n_unit_test.rb +7 -5
  104. data/test/unit/lexer_unit_test.rb +11 -9
  105. data/test/{integration → unit}/parse_tree_visitor_test.rb +9 -2
  106. data/test/unit/parser_unit_test.rb +37 -35
  107. data/test/unit/partial_cache_unit_test.rb +128 -0
  108. data/test/unit/regexp_unit_test.rb +17 -15
  109. data/test/unit/static_registers_unit_test.rb +156 -0
  110. data/test/unit/strainer_factory_unit_test.rb +100 -0
  111. data/test/unit/strainer_template_unit_test.rb +82 -0
  112. data/test/unit/tag_unit_test.rb +5 -3
  113. data/test/unit/tags/case_tag_unit_test.rb +3 -1
  114. data/test/unit/tags/for_tag_unit_test.rb +4 -2
  115. data/test/unit/tags/if_tag_unit_test.rb +3 -1
  116. data/test/unit/template_factory_unit_test.rb +12 -0
  117. data/test/unit/template_unit_test.rb +19 -10
  118. data/test/unit/tokenizer_unit_test.rb +26 -19
  119. data/test/unit/variable_unit_test.rb +51 -49
  120. metadata +73 -47
  121. data/lib/liquid/strainer.rb +0 -66
  122. data/lib/liquid/truffle.rb +0 -5
  123. data/test/truffle/truffle_test.rb +0 -9
  124. data/test/unit/context_unit_test.rb +0 -489
  125. 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
- return strict_parse_with_error_context(markup)
23
+ strict_parse_with_error_context(markup)
10
24
  rescue SyntaxError => e
11
25
  parse_context.warnings << e
12
- return lax_parse(markup)
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 = 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,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
- # # If top level, this will be "<root>".
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, :partial, :line_number, :children
48
-
49
- def initialize(node, partial)
50
- @code = node.respond_to?(:raw) ? node.raw : node
51
- @partial = partial
52
- @line_number = node.respond_to?(:line_number) ? node.line_number : nil
53
- @children = []
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 self.start(node, partial)
57
- new(node, partial).tap(&:start)
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
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
- def self.profile_node_render(node)
74
- if Profiler.current_profile && node.respond_to?(:render)
75
- Profiler.current_profile.start_node(node)
76
- output = yield
77
- Profiler.current_profile.end_node(node)
78
- output
79
- else
80
- yield
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 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
98
+ def children
99
+ children = @root_children
100
+ if children.length == 1
101
+ children.first.children
90
102
  else
91
- yield
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
- @root_timing.children.each(&block)
108
+ children.each(&block)
125
109
  end
126
110
 
127
111
  def [](idx)
128
- @root_timing.children[idx]
112
+ children[idx]
129
113
  end
130
114
 
131
115
  def length
132
- @root_timing.children.length
116
+ children.length
133
117
  end
134
118
 
135
- def start_node(node)
136
- @timing_stack.push(Timing.start(node, current_partial))
137
- end
138
-
139
- def end_node(_node)
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
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
- def push_partial(partial_name)
151
- @partial_stack.push(partial_name)
152
- end
133
+ private
153
134
 
154
- def pop_partial
155
- @partial_stack.pop
135
+ def monotonic_time
136
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
156
137
  end
157
138
  end
158
139
  end
@@ -1,23 +1,35 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- class BlockBody
3
- def render_node_with_profiling(node, output, context, skip_output = false)
4
- Profiler.profile_node_render(node) do
5
- render_node_without_profiling(node, output, context, skip_output)
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
- class Include < Tag
14
- def render_with_profiling(context)
15
- Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
16
- render_without_profiling(context)
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
- alias_method :render_without_profiling, :render
21
- alias_method :render, :render_with_profiling
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
@@ -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 = Expression.parse(end_markup)
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
@@ -12,12 +14,12 @@ module Liquid
12
14
 
13
15
  def initialize(start_obj, end_obj)
14
16
  @start_obj = start_obj
15
- @end_obj = end_obj
17
+ @end_obj = end_obj
16
18
  end
17
19
 
18
20
  def evaluate(context)
19
21
  start_int = to_integer(context.evaluate(@start_obj))
20
- end_int = to_integer(context.evaluate(@end_obj))
22
+ end_int = to_integer(context.evaluate(@end_obj))
21
23
  start_int..end_int
22
24
  end
23
25
 
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class Register
5
+ end
6
+ end
@@ -1,23 +1,62 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class ResourceLimits
3
- attr_accessor :render_length, :render_score, :assign_score,
4
- :render_length_limit, :render_score_limit, :assign_score_limit
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 = limits[:render_score_limit]
9
- @assign_score_limit = limits[: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
- (@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)
43
+ @reached_limit
17
44
  end
18
45
 
19
46
  def reset
20
- @render_length = @render_score = @assign_score = 0
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,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cgi'
2
4
  require 'bigdecimal'
3
5
 
4
6
  module Liquid
5
7
  module StandardFilters
6
8
  HTML_ESCAPE = {
7
- '&'.freeze => '&amp;'.freeze,
8
- '>'.freeze => '&gt;'.freeze,
9
- '<'.freeze => '&lt;'.freeze,
10
- '"'.freeze => '&quot;'.freeze,
11
- "'".freeze => '&#39;'.freeze
9
+ '&' => '&amp;',
10
+ '>' => '&gt;',
11
+ '<' => '&lt;',
12
+ '"' => '&quot;',
13
+ "'" => '&#39;',
12
14
  }.freeze
13
15
  HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
14
- STRIP_HTML_BLOCKS = Regexp.union(
15
- /<script.*?<\/script>/m,
16
+ STRIP_HTML_BLOCKS = Regexp.union(
17
+ %r{<script.*?</script>}m,
16
18
  /<!--.*?-->/m,
17
- /<style.*?<\/style>/m
19
+ %r{<style.*?</style>}m
18
20
  )
19
21
  STRIP_HTML_TAGS = /<.*?>/m
20
22
 
@@ -39,7 +41,7 @@ module Liquid
39
41
  end
40
42
 
41
43
  def escape(input)
42
- CGI.escapeHTML(input.to_s).untaint unless input.nil?
44
+ CGI.escapeHTML(input.to_s) unless input.nil?
43
45
  end
44
46
  alias_method :h, :escape
45
47
 
@@ -72,23 +74,30 @@ module Liquid
72
74
  end
73
75
 
74
76
  # Truncate a string down to x characters
75
- def truncate(input, length = 50, truncate_string = "...".freeze)
77
+ def truncate(input, length = 50, truncate_string = "...")
76
78
  return if input.nil?
77
79
  input_str = input.to_s
78
- length = Utils.to_integer(length)
80
+ length = Utils.to_integer(length)
81
+
79
82
  truncate_string_str = truncate_string.to_s
83
+
80
84
  l = length - truncate_string_str.length
81
85
  l = 0 if l < 0
82
- input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
86
+
87
+ input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
83
88
  end
84
89
 
85
- def truncatewords(input, words = 15, truncate_string = "...".freeze)
90
+ def truncatewords(input, words = 15, truncate_string = "...")
86
91
  return if input.nil?
87
- wordlist = input.to_s.split
92
+ input = input.to_s
88
93
  words = Utils.to_integer(words)
89
- l = words - 1
90
- l = 0 if l < 0
91
- wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input
94
+ words = 1 if words <= 0
95
+
96
+ wordlist = input.split(" ", words + 1)
97
+ return input if wordlist.length <= words
98
+
99
+ wordlist.pop
100
+ wordlist.join(" ").concat(truncate_string.to_s)
92
101
  end
93
102
 
94
103
  # Split input string into an array of substrings separated by given pattern.
@@ -113,7 +122,7 @@ module Liquid
113
122
  end
114
123
 
115
124
  def strip_html(input)
116
- empty = ''.freeze
125
+ empty = ''
117
126
  result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
118
127
  result.gsub!(STRIP_HTML_TAGS, empty)
119
128
  result
@@ -121,18 +130,18 @@ module Liquid
121
130
 
122
131
  # Remove all newlines from the string
123
132
  def strip_newlines(input)
124
- input.to_s.gsub(/\r?\n/, ''.freeze)
133
+ input.to_s.gsub(/\r?\n/, '')
125
134
  end
126
135
 
127
136
  # Join elements of the array with certain character between them
128
- def join(input, glue = ' '.freeze)
129
- InputIterator.new(input).join(glue)
137
+ def join(input, glue = ' ')
138
+ InputIterator.new(input, context).join(glue)
130
139
  end
131
140
 
132
141
  # Sort elements of the array
133
142
  # provide optional property with which to sort an array of hashes or drops
134
143
  def sort(input, property = nil)
135
- ary = InputIterator.new(input)
144
+ ary = InputIterator.new(input, context)
136
145
 
137
146
  return [] if ary.empty?
138
147
 
@@ -152,7 +161,7 @@ module Liquid
152
161
  # Sort elements of an array ignoring case if strings
153
162
  # provide optional property with which to sort an array of hashes or drops
154
163
  def sort_natural(input, property = nil)
155
- ary = InputIterator.new(input)
164
+ ary = InputIterator.new(input, context)
156
165
 
157
166
  return [] if ary.empty?
158
167
 
@@ -172,7 +181,7 @@ module Liquid
172
181
  # Filter the elements of an array to those with a certain property value.
173
182
  # By default the target is any truthy value.
174
183
  def where(input, property, target_value = nil)
175
- ary = InputIterator.new(input)
184
+ ary = InputIterator.new(input, context)
176
185
 
177
186
  if ary.empty?
178
187
  []
@@ -194,7 +203,7 @@ module Liquid
194
203
  # Remove duplicate elements from an array
195
204
  # provide optional property with which to determine uniqueness
196
205
  def uniq(input, property = nil)
197
- ary = InputIterator.new(input)
206
+ ary = InputIterator.new(input, context)
198
207
 
199
208
  if property.nil?
200
209
  ary.uniq
@@ -211,16 +220,16 @@ module Liquid
211
220
 
212
221
  # Reverse the elements of an array
213
222
  def reverse(input)
214
- ary = InputIterator.new(input)
223
+ ary = InputIterator.new(input, context)
215
224
  ary.reverse
216
225
  end
217
226
 
218
227
  # map/collect on a given property
219
228
  def map(input, property)
220
- InputIterator.new(input).map do |e|
229
+ InputIterator.new(input, context).map do |e|
221
230
  e = e.call if e.is_a?(Proc)
222
231
 
223
- if property == "to_liquid".freeze
232
+ if property == "to_liquid"
224
233
  e
225
234
  elsif e.respond_to?(:[])
226
235
  r = e[property]
@@ -234,7 +243,7 @@ module Liquid
234
243
  # Remove nils within an array
235
244
  # provide optional property with which to check for nil
236
245
  def compact(input, property = nil)
237
- ary = InputIterator.new(input)
246
+ ary = InputIterator.new(input, context)
238
247
 
239
248
  if property.nil?
240
249
  ary.compact
@@ -250,23 +259,23 @@ module Liquid
250
259
  end
251
260
 
252
261
  # Replace occurrences of a string with another
253
- def replace(input, string, replacement = ''.freeze)
262
+ def replace(input, string, replacement = '')
254
263
  input.to_s.gsub(string.to_s, replacement.to_s)
255
264
  end
256
265
 
257
266
  # Replace the first occurrences of a string with another
258
- def replace_first(input, string, replacement = ''.freeze)
267
+ def replace_first(input, string, replacement = '')
259
268
  input.to_s.sub(string.to_s, replacement.to_s)
260
269
  end
261
270
 
262
271
  # remove a substring
263
272
  def remove(input, string)
264
- input.to_s.gsub(string.to_s, ''.freeze)
273
+ input.to_s.gsub(string.to_s, '')
265
274
  end
266
275
 
267
276
  # remove the first occurrences of a substring
268
277
  def remove_first(input, string)
269
- input.to_s.sub(string.to_s, ''.freeze)
278
+ input.to_s.sub(string.to_s, '')
270
279
  end
271
280
 
272
281
  # add one string to another
@@ -276,9 +285,9 @@ module Liquid
276
285
 
277
286
  def concat(input, array)
278
287
  unless array.respond_to?(:to_ary)
279
- raise ArgumentError.new("concat filter requires an array argument")
288
+ raise ArgumentError, "concat filter requires an array argument"
280
289
  end
281
- InputIterator.new(input).concat(array)
290
+ InputIterator.new(input, context).concat(array)
282
291
  end
283
292
 
284
293
  # prepend a string to another
@@ -288,7 +297,7 @@ module Liquid
288
297
 
289
298
  # Add <br /> tags in front of all newlines in input string
290
299
  def newline_to_br(input)
291
- input.to_s.gsub(/\n/, "<br />\n".freeze)
300
+ input.to_s.gsub(/\r?\n/, "<br />\n")
292
301
  end
293
302
 
294
303
  # Reformat a date using Ruby's core Time#strftime( string ) -> string
@@ -325,7 +334,7 @@ module Liquid
325
334
  def date(input, format)
326
335
  return input if format.to_s.empty?
327
336
 
328
- return input unless date = Utils.to_date(input)
337
+ return input unless (date = Utils.to_date(input))
329
338
 
330
339
  date.strftime(format.to_s)
331
340
  end
@@ -419,18 +428,28 @@ module Liquid
419
428
  result.is_a?(BigDecimal) ? result.to_f : result
420
429
  end
421
430
 
422
- def default(input, default_value = ''.freeze)
423
- if !input || input.respond_to?(:empty?) && input.empty?
424
- default_value
425
- else
426
- input
427
- end
431
+ # Set a default value when the input is nil, false or empty
432
+ #
433
+ # Example:
434
+ # {{ product.title | default: "No Title" }}
435
+ #
436
+ # Use `allow_false` when an input should only be tested against nil or empty and not false.
437
+ #
438
+ # Example:
439
+ # {{ product.title | default: "No Title", allow_false: true }}
440
+ #
441
+ def default(input, default_value = '', options = {})
442
+ options = {} unless options.is_a?(Hash)
443
+ false_check = options['allow_false'] ? input.nil? : !input
444
+ false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
428
445
  end
429
446
 
430
447
  private
431
448
 
449
+ attr_reader :context
450
+
432
451
  def raise_property_error(property)
433
- raise Liquid::ArgumentError.new("cannot select the property '#{property}'")
452
+ raise Liquid::ArgumentError, "cannot select the property '#{property}'"
434
453
  end
435
454
 
436
455
  def apply_operation(input, operand, operation)
@@ -457,8 +476,9 @@ module Liquid
457
476
  class InputIterator
458
477
  include Enumerable
459
478
 
460
- def initialize(input)
461
- @input = if input.is_a?(Array)
479
+ def initialize(input, context)
480
+ @context = context
481
+ @input = if input.is_a?(Array)
462
482
  input.flatten
463
483
  elsif input.is_a?(Hash)
464
484
  [input]
@@ -496,6 +516,7 @@ module Liquid
496
516
 
497
517
  def each
498
518
  @input.each do |e|
519
+ e.context = @context if e.respond_to?(:context=)
499
520
  yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
500
521
  end
501
522
  end