liquid 4.0.2 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +59 -0
  3. data/README.md +6 -0
  4. data/lib/liquid/block.rb +31 -14
  5. data/lib/liquid/block_body.rb +166 -53
  6. data/lib/liquid/condition.rb +41 -20
  7. data/lib/liquid/context.rb +107 -52
  8. data/lib/liquid/document.rb +47 -9
  9. data/lib/liquid/drop.rb +4 -2
  10. data/lib/liquid/errors.rb +20 -18
  11. data/lib/liquid/expression.rb +29 -34
  12. data/lib/liquid/extensions.rb +2 -0
  13. data/lib/liquid/file_system.rb +6 -4
  14. data/lib/liquid/forloop_drop.rb +11 -4
  15. data/lib/liquid/i18n.rb +5 -3
  16. data/lib/liquid/interrupts.rb +3 -1
  17. data/lib/liquid/lexer.rb +30 -23
  18. data/lib/liquid/locales/en.yml +3 -1
  19. data/lib/liquid/parse_context.rb +20 -4
  20. data/lib/liquid/parse_tree_visitor.rb +2 -2
  21. data/lib/liquid/parser.rb +30 -18
  22. data/lib/liquid/parser_switching.rb +17 -3
  23. data/lib/liquid/partial_cache.rb +24 -0
  24. data/lib/liquid/profiler/hooks.rb +26 -14
  25. data/lib/liquid/profiler.rb +67 -86
  26. data/lib/liquid/range_lookup.rb +13 -3
  27. data/lib/liquid/register.rb +6 -0
  28. data/lib/liquid/resource_limits.rb +47 -8
  29. data/lib/liquid/standardfilters.rb +95 -46
  30. data/lib/liquid/static_registers.rb +44 -0
  31. data/lib/liquid/strainer_factory.rb +36 -0
  32. data/lib/liquid/strainer_template.rb +53 -0
  33. data/lib/liquid/tablerowloop_drop.rb +6 -4
  34. data/lib/liquid/tag/disableable.rb +22 -0
  35. data/lib/liquid/tag/disabler.rb +21 -0
  36. data/lib/liquid/tag.rb +28 -6
  37. data/lib/liquid/tags/assign.rb +24 -10
  38. data/lib/liquid/tags/break.rb +8 -3
  39. data/lib/liquid/tags/capture.rb +11 -8
  40. data/lib/liquid/tags/case.rb +40 -27
  41. data/lib/liquid/tags/comment.rb +5 -3
  42. data/lib/liquid/tags/continue.rb +8 -3
  43. data/lib/liquid/tags/cycle.rb +25 -14
  44. data/lib/liquid/tags/decrement.rb +6 -3
  45. data/lib/liquid/tags/echo.rb +34 -0
  46. data/lib/liquid/tags/for.rb +68 -44
  47. data/lib/liquid/tags/if.rb +39 -23
  48. data/lib/liquid/tags/ifchanged.rb +11 -10
  49. data/lib/liquid/tags/include.rb +34 -47
  50. data/lib/liquid/tags/increment.rb +7 -3
  51. data/lib/liquid/tags/raw.rb +14 -11
  52. data/lib/liquid/tags/render.rb +84 -0
  53. data/lib/liquid/tags/table_row.rb +23 -19
  54. data/lib/liquid/tags/unless.rb +23 -15
  55. data/lib/liquid/template.rb +53 -72
  56. data/lib/liquid/template_factory.rb +9 -0
  57. data/lib/liquid/tokenizer.rb +18 -10
  58. data/lib/liquid/usage.rb +8 -0
  59. data/lib/liquid/utils.rb +13 -3
  60. data/lib/liquid/variable.rb +46 -41
  61. data/lib/liquid/variable_lookup.rb +11 -6
  62. data/lib/liquid/version.rb +2 -1
  63. data/lib/liquid.rb +17 -5
  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 +385 -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 +76 -52
  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 +132 -124
  97. data/test/integration/trim_mode_test.rb +78 -44
  98. data/test/integration/variable_test.rb +74 -32
  99. data/test/test_helper.rb +113 -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 +16 -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 +76 -50
  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,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,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,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
@@ -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 = 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 = to_integer(context.evaluate(@end_obj))
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
@@ -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,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
- '&'.freeze => '&amp;'.freeze,
8
- '>'.freeze => '&gt;'.freeze,
9
- '<'.freeze => '&lt;'.freeze,
10
- '"'.freeze => '&quot;'.freeze,
11
- "'".freeze => '&#39;'.freeze
11
+ '&' => '&amp;',
12
+ '>' => '&gt;',
13
+ '<' => '&lt;',
14
+ '"' => '&quot;',
15
+ "'" => '&#39;',
12
16
  }.freeze
13
17
  HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
14
- STRIP_HTML_BLOCKS = Regexp.union(
15
- /<script.*?<\/script>/m,
18
+ STRIP_HTML_BLOCKS = Regexp.union(
19
+ %r{<script.*?</script>}m,
16
20
  /<!--.*?-->/m,
17
- /<style.*?<\/style>/m
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).untaint unless input.nil?
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 = "...".freeze)
99
+ def truncate(input, length = 50, truncate_string = "...")
76
100
  return if input.nil?
77
101
  input_str = input.to_s
78
- length = Utils.to_integer(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
- input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
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 = "...".freeze)
112
+ def truncatewords(input, words = 15, truncate_string = "...")
86
113
  return if input.nil?
87
- wordlist = input.to_s.split
114
+ input = input.to_s
88
115
  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
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 = ''.freeze
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/, ''.freeze)
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 = ' '.freeze)
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".freeze
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 = ''.freeze)
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 = ''.freeze)
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, ''.freeze)
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, ''.freeze)
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.new("concat filter requires an array argument")
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".freeze)
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
- def default(input, default_value = ''.freeze)
423
- if !input || input.respond_to?(:empty?) && input.empty?
424
- default_value
425
- else
426
- input
427
- end
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.new("cannot select the property '#{property}'")
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
- @input = if input.is_a?(Array)
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