liquid 3.0.6 → 5.4.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.
Files changed (113) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +243 -58
  3. data/README.md +43 -4
  4. data/lib/liquid/block.rb +57 -123
  5. data/lib/liquid/block_body.rb +217 -85
  6. data/lib/liquid/condition.rb +92 -45
  7. data/lib/liquid/context.rb +154 -89
  8. data/lib/liquid/document.rb +57 -9
  9. data/lib/liquid/drop.rb +20 -17
  10. data/lib/liquid/errors.rb +27 -29
  11. data/lib/liquid/expression.rb +32 -20
  12. data/lib/liquid/extensions.rb +21 -7
  13. data/lib/liquid/file_system.rb +17 -15
  14. data/lib/liquid/forloop_drop.rb +92 -0
  15. data/lib/liquid/i18n.rb +10 -8
  16. data/lib/liquid/interrupts.rb +4 -3
  17. data/lib/liquid/lexer.rb +37 -26
  18. data/lib/liquid/locales/en.yml +13 -6
  19. data/lib/liquid/parse_context.rb +54 -0
  20. data/lib/liquid/parse_tree_visitor.rb +42 -0
  21. data/lib/liquid/parser.rb +30 -18
  22. data/lib/liquid/parser_switching.rb +20 -6
  23. data/lib/liquid/partial_cache.rb +24 -0
  24. data/lib/liquid/profiler/hooks.rb +26 -14
  25. data/lib/liquid/profiler.rb +72 -92
  26. data/lib/liquid/range_lookup.rb +28 -3
  27. data/lib/liquid/registers.rb +51 -0
  28. data/lib/liquid/resource_limits.rb +62 -0
  29. data/lib/liquid/standardfilters.rb +715 -132
  30. data/lib/liquid/strainer_factory.rb +41 -0
  31. data/lib/liquid/strainer_template.rb +62 -0
  32. data/lib/liquid/tablerowloop_drop.rb +121 -0
  33. data/lib/liquid/tag/disableable.rb +22 -0
  34. data/lib/liquid/tag/disabler.rb +21 -0
  35. data/lib/liquid/tag.rb +35 -12
  36. data/lib/liquid/tags/assign.rb +57 -18
  37. data/lib/liquid/tags/break.rb +15 -5
  38. data/lib/liquid/tags/capture.rb +24 -18
  39. data/lib/liquid/tags/case.rb +79 -30
  40. data/lib/liquid/tags/comment.rb +19 -4
  41. data/lib/liquid/tags/continue.rb +16 -12
  42. data/lib/liquid/tags/cycle.rb +47 -27
  43. data/lib/liquid/tags/decrement.rb +23 -24
  44. data/lib/liquid/tags/echo.rb +41 -0
  45. data/lib/liquid/tags/for.rb +155 -124
  46. data/lib/liquid/tags/if.rb +97 -63
  47. data/lib/liquid/tags/ifchanged.rb +11 -12
  48. data/lib/liquid/tags/include.rb +82 -73
  49. data/lib/liquid/tags/increment.rb +23 -17
  50. data/lib/liquid/tags/inline_comment.rb +43 -0
  51. data/lib/liquid/tags/raw.rb +50 -8
  52. data/lib/liquid/tags/render.rb +109 -0
  53. data/lib/liquid/tags/table_row.rb +57 -41
  54. data/lib/liquid/tags/unless.rb +38 -20
  55. data/lib/liquid/template.rb +71 -103
  56. data/lib/liquid/template_factory.rb +9 -0
  57. data/lib/liquid/tokenizer.rb +39 -0
  58. data/lib/liquid/usage.rb +8 -0
  59. data/lib/liquid/utils.rb +63 -9
  60. data/lib/liquid/variable.rb +74 -56
  61. data/lib/liquid/variable_lookup.rb +31 -15
  62. data/lib/liquid/version.rb +3 -1
  63. data/lib/liquid.rb +27 -12
  64. metadata +30 -106
  65. data/lib/liquid/module_ex.rb +0 -62
  66. data/lib/liquid/strainer.rb +0 -59
  67. data/lib/liquid/token.rb +0 -18
  68. data/test/fixtures/en_locale.yml +0 -9
  69. data/test/integration/assign_test.rb +0 -48
  70. data/test/integration/blank_test.rb +0 -106
  71. data/test/integration/capture_test.rb +0 -50
  72. data/test/integration/context_test.rb +0 -32
  73. data/test/integration/drop_test.rb +0 -271
  74. data/test/integration/error_handling_test.rb +0 -207
  75. data/test/integration/filter_test.rb +0 -138
  76. data/test/integration/hash_ordering_test.rb +0 -23
  77. data/test/integration/output_test.rb +0 -124
  78. data/test/integration/parsing_quirks_test.rb +0 -116
  79. data/test/integration/render_profiling_test.rb +0 -154
  80. data/test/integration/security_test.rb +0 -64
  81. data/test/integration/standard_filter_test.rb +0 -396
  82. data/test/integration/tags/break_tag_test.rb +0 -16
  83. data/test/integration/tags/continue_tag_test.rb +0 -16
  84. data/test/integration/tags/for_tag_test.rb +0 -375
  85. data/test/integration/tags/if_else_tag_test.rb +0 -190
  86. data/test/integration/tags/include_tag_test.rb +0 -234
  87. data/test/integration/tags/increment_tag_test.rb +0 -24
  88. data/test/integration/tags/raw_tag_test.rb +0 -25
  89. data/test/integration/tags/standard_tag_test.rb +0 -297
  90. data/test/integration/tags/statements_test.rb +0 -113
  91. data/test/integration/tags/table_row_test.rb +0 -63
  92. data/test/integration/tags/unless_else_tag_test.rb +0 -26
  93. data/test/integration/template_test.rb +0 -182
  94. data/test/integration/variable_test.rb +0 -82
  95. data/test/test_helper.rb +0 -89
  96. data/test/unit/block_unit_test.rb +0 -55
  97. data/test/unit/condition_unit_test.rb +0 -149
  98. data/test/unit/context_unit_test.rb +0 -492
  99. data/test/unit/file_system_unit_test.rb +0 -35
  100. data/test/unit/i18n_unit_test.rb +0 -37
  101. data/test/unit/lexer_unit_test.rb +0 -48
  102. data/test/unit/module_ex_unit_test.rb +0 -87
  103. data/test/unit/parser_unit_test.rb +0 -82
  104. data/test/unit/regexp_unit_test.rb +0 -44
  105. data/test/unit/strainer_unit_test.rb +0 -69
  106. data/test/unit/tag_unit_test.rb +0 -16
  107. data/test/unit/tags/case_tag_unit_test.rb +0 -10
  108. data/test/unit/tags/for_tag_unit_test.rb +0 -13
  109. data/test/unit/tags/if_tag_unit_test.rb +0 -8
  110. data/test/unit/template_unit_test.rb +0 -69
  111. data/test/unit/tokenizer_unit_test.rb +0 -38
  112. data/test/unit/variable_unit_test.rb +0 -145
  113. /data/{MIT-LICENSE → LICENSE} +0 -0
data/lib/liquid/block.rb CHANGED
@@ -1,93 +1,57 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Block < Tag
3
- FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
4
- ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
5
- TAGSTART = "{%".freeze
6
- VARSTART = "{{".freeze
5
+ MAX_DEPTH = 100
7
6
 
8
- def blank?
9
- @blank
7
+ def initialize(tag_name, markup, options)
8
+ super
9
+ @blank = true
10
10
  end
11
11
 
12
12
  def parse(tokens)
13
- @blank = true
14
- @nodelist ||= []
15
- @nodelist.clear
16
-
17
- while token = tokens.shift
18
- begin
19
- unless token.empty?
20
- case
21
- when token.start_with?(TAGSTART)
22
- if token =~ FullToken
23
-
24
- # if we found the proper block delimiter just end parsing here and let the outer block
25
- # proceed
26
- return if block_delimiter == $1
27
-
28
- # fetch the tag from registered blocks
29
- if tag = Template.tags[$1]
30
- markup = token.is_a?(Token) ? token.child($2) : $2
31
- new_tag = tag.parse($1, markup, tokens, @options)
32
- new_tag.line_number = token.line_number if token.is_a?(Token)
33
- @blank &&= new_tag.blank?
34
- @nodelist << new_tag
35
- else
36
- # this tag is not registered with the system
37
- # pass it to the current block for special handling or error reporting
38
- unknown_tag($1, $2, tokens)
39
- end
40
- else
41
- raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
42
- end
43
- when token.start_with?(VARSTART)
44
- new_var = create_variable(token)
45
- new_var.line_number = token.line_number if token.is_a?(Token)
46
- @nodelist << new_var
47
- @blank = false
48
- else
49
- @nodelist << token
50
- @blank &&= (token =~ /\A\s*\z/)
51
- end
52
- end
53
- rescue SyntaxError => e
54
- e.set_line_number_from_token(token)
55
- raise
56
- end
13
+ @body = new_body
14
+ while parse_body(@body, tokens)
57
15
  end
16
+ @body.freeze
17
+ end
58
18
 
59
- # Make sure that it's ok to end parsing in the current block.
60
- # Effectively this method will throw an exception unless the current block is
61
- # of type Document
62
- assert_missing_delimitation!
19
+ # For backwards compatibility
20
+ def render(context)
21
+ @body.render(context)
63
22
  end
64
23
 
65
- # warnings of this block and all sub-tags
66
- def warnings
67
- all_warnings = []
68
- all_warnings.concat(@warnings) if @warnings
24
+ def blank?
25
+ @blank
26
+ end
69
27
 
70
- (nodelist || []).each do |node|
71
- all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
72
- end
28
+ def nodelist
29
+ @body.nodelist
30
+ end
73
31
 
74
- all_warnings
32
+ def unknown_tag(tag_name, _markup, _tokenizer)
33
+ Block.raise_unknown_tag(tag_name, block_name, block_delimiter, parse_context)
75
34
  end
76
35
 
77
- def unknown_tag(tag, params, tokens)
78
- case tag
79
- when 'else'.freeze
80
- raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else".freeze,
81
- :block_name => block_name))
82
- when 'end'.freeze
83
- raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter".freeze,
84
- :block_name => block_name,
85
- :block_delimiter => block_delimiter))
36
+ # @api private
37
+ def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
38
+ if tag == 'else'
39
+ raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
40
+ block_name: block_name)
41
+ elsif tag.start_with?('end')
42
+ raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter",
43
+ tag: tag,
44
+ block_name: block_name,
45
+ block_delimiter: block_delimiter)
86
46
  else
87
- raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag".freeze, :tag => tag))
47
+ raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
88
48
  end
89
49
  end
90
50
 
51
+ def raise_tag_never_closed(block_name)
52
+ raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
53
+ end
54
+
91
55
  def block_name
92
56
  @tag_name
93
57
  end
@@ -96,65 +60,35 @@ module Liquid
96
60
  @block_delimiter ||= "end#{block_name}"
97
61
  end
98
62
 
99
- def create_variable(token)
100
- token.scan(ContentOfVariable) do |content|
101
- markup = token.is_a?(Token) ? token.child(content.first) : content.first
102
- return Variable.new(markup, @options)
103
- end
104
- raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
105
- end
63
+ private
106
64
 
107
- def render(context)
108
- render_all(@nodelist, context)
65
+ # @api public
66
+ def new_body
67
+ parse_context.new_block_body
109
68
  end
110
69
 
111
- protected
70
+ # @api public
71
+ def parse_body(body, tokens)
72
+ if parse_context.depth >= MAX_DEPTH
73
+ raise StackLevelError, "Nesting too deep"
74
+ end
75
+ parse_context.depth += 1
76
+ begin
77
+ body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
78
+ @blank &&= body.blank?
112
79
 
113
- def assert_missing_delimitation!
114
- raise SyntaxError.new(options[:locale].t("errors.syntax.tag_never_closed".freeze, :block_name => block_name))
115
- end
80
+ return false if end_tag_name == block_delimiter
81
+ raise_tag_never_closed(block_name) unless end_tag_name
116
82
 
117
- def render_all(list, context)
118
- output = []
119
- context.resource_limits[:render_length_current] = 0
120
- context.resource_limits[:render_score_current] += list.length
121
-
122
- list.each do |token|
123
- # Break out if we have any unhanded interrupts.
124
- break if context.has_interrupt?
125
-
126
- begin
127
- # If we get an Interrupt that means the block must stop processing. An
128
- # Interrupt is any command that stops block execution such as {% break %}
129
- # or {% continue %}
130
- if token.is_a? Continue or token.is_a? Break
131
- context.push_interrupt(token.interrupt)
132
- break
133
- end
134
-
135
- token_output = render_token(token, context)
136
-
137
- unless token.is_a?(Block) && token.blank?
138
- output << token_output
139
- end
140
- rescue MemoryError => e
141
- raise e
142
- rescue ::StandardError => e
143
- output << (context.handle_error(e, token))
83
+ # this tag is not registered with the system
84
+ # pass it to the current block for special handling or error reporting
85
+ unknown_tag(end_tag_name, end_tag_params, tokens)
144
86
  end
87
+ ensure
88
+ parse_context.depth -= 1
145
89
  end
146
90
 
147
- output.join
148
- end
149
-
150
- def render_token(token, context)
151
- token_output = (token.respond_to?(:render) ? token.render(context) : token)
152
- context.increment_used_resources(:render_length_current, token_output)
153
- if context.resource_limits_reached?
154
- context.resource_limits[:reached] = true
155
- raise MemoryError.new("Memory limits exceeded".freeze)
156
- end
157
- token_output
91
+ true
158
92
  end
159
93
  end
160
94
  end
@@ -1,123 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
1
5
  module Liquid
2
6
  class BlockBody
3
- FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
4
- ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
5
- TAGSTART = "{%".freeze
6
- VARSTART = "{{".freeze
7
+ LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o
8
+ FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
9
+ ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
10
+ WhitespaceOrNothing = /\A\s*\z/
11
+ TAGSTART = "{%"
12
+ VARSTART = "{{"
7
13
 
8
14
  attr_reader :nodelist
9
15
 
10
16
  def initialize
11
17
  @nodelist = []
12
- @blank = true
13
- end
14
-
15
- def parse(tokens, options)
16
- while token = tokens.shift
17
- begin
18
- unless token.empty?
19
- case
20
- when token.start_with?(TAGSTART)
21
- if token =~ FullToken
22
- tag_name = $1
23
- markup = $2
24
- # fetch the tag from registered blocks
25
- if tag = Template.tags[tag_name]
26
- markup = token.child(markup) if token.is_a?(Token)
27
- new_tag = tag.parse(tag_name, markup, tokens, options)
28
- new_tag.line_number = token.line_number if token.is_a?(Token)
29
- @blank &&= new_tag.blank?
30
- @nodelist << new_tag
31
- else
32
- # end parsing if we reach an unknown tag and let the caller decide
33
- # determine how to proceed
34
- return yield tag_name, markup
35
- end
36
- else
37
- raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
38
- end
39
- when token.start_with?(VARSTART)
40
- new_var = create_variable(token, options)
41
- new_var.line_number = token.line_number if token.is_a?(Token)
42
- @nodelist << new_var
43
- @blank = false
44
- else
45
- @nodelist << token
46
- @blank &&= !!(token =~ /\A\s*\z/)
47
- end
18
+ @blank = true
19
+ end
20
+
21
+ def parse(tokenizer, parse_context, &block)
22
+ raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
23
+
24
+ parse_context.line_number = tokenizer.line_number
25
+
26
+ if tokenizer.for_liquid_tag
27
+ parse_for_liquid_tag(tokenizer, parse_context, &block)
28
+ else
29
+ parse_for_document(tokenizer, parse_context, &block)
30
+ end
31
+ end
32
+
33
+ def freeze
34
+ @nodelist.freeze
35
+ super
36
+ end
37
+
38
+ private def parse_for_liquid_tag(tokenizer, parse_context)
39
+ while (token = tokenizer.shift)
40
+ unless token.empty? || token.match?(WhitespaceOrNothing)
41
+ unless token =~ LiquidTagToken
42
+ # line isn't empty but didn't match tag syntax, yield and let the
43
+ # caller raise a syntax error
44
+ return yield token, token
45
+ end
46
+ tag_name = Regexp.last_match(1)
47
+ markup = Regexp.last_match(2)
48
+ unless (tag = registered_tags[tag_name])
49
+ # end parsing if we reach an unknown tag and let the caller decide
50
+ # determine how to proceed
51
+ return yield tag_name, markup
48
52
  end
49
- rescue SyntaxError => e
50
- e.set_line_number_from_token(token)
51
- raise
53
+ new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
54
+ @blank &&= new_tag.blank?
55
+ @nodelist << new_tag
52
56
  end
57
+ parse_context.line_number = tokenizer.line_number
53
58
  end
54
59
 
55
60
  yield nil, nil
56
61
  end
57
62
 
58
- def blank?
59
- @blank
63
+ # @api private
64
+ def self.unknown_tag_in_liquid_tag(tag, parse_context)
65
+ Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
66
+ end
67
+
68
+ # @api private
69
+ def self.raise_missing_tag_terminator(token, parse_context)
70
+ raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
60
71
  end
61
72
 
62
- def warnings
63
- all_warnings = []
64
- nodelist.each do |node|
65
- all_warnings.concat(node.warnings) if node.respond_to?(:warnings) && node.warnings
73
+ # @api private
74
+ def self.raise_missing_variable_terminator(token, parse_context)
75
+ raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
76
+ end
77
+
78
+ # @api private
79
+ def self.render_node(context, output, node)
80
+ node.render_to_output_buffer(context, output)
81
+ rescue => exc
82
+ blank_tag = !node.instance_of?(Variable) && node.blank?
83
+ rescue_render_node(context, output, node.line_number, exc, blank_tag)
84
+ end
85
+
86
+ # @api private
87
+ def self.rescue_render_node(context, output, line_number, exc, blank_tag)
88
+ case exc
89
+ when MemoryError
90
+ raise
91
+ when UndefinedVariable, UndefinedDropMethod, UndefinedFilter
92
+ context.handle_error(exc, line_number)
93
+ else
94
+ error_message = context.handle_error(exc, line_number)
95
+ unless blank_tag # conditional for backwards compatibility
96
+ output << error_message
97
+ end
66
98
  end
67
- all_warnings
68
99
  end
69
100
 
70
- def render(context)
71
- output = []
72
- context.resource_limits[:render_length_current] = 0
73
- context.resource_limits[:render_score_current] += @nodelist.length
101
+ private def parse_liquid_tag(markup, parse_context)
102
+ liquid_tag_tokenizer = parse_context.new_tokenizer(
103
+ markup, start_line_number: parse_context.line_number, for_liquid_tag: true
104
+ )
105
+ parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
106
+ if end_tag_name
107
+ BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
108
+ end
109
+ end
110
+ end
74
111
 
75
- @nodelist.each do |token|
76
- # Break out if we have any unhanded interrupts.
77
- break if context.has_interrupt?
112
+ private def parse_for_document(tokenizer, parse_context)
113
+ while (token = tokenizer.shift)
114
+ next if token.empty?
115
+ case
116
+ when token.start_with?(TAGSTART)
117
+ whitespace_handler(token, parse_context)
118
+ unless token =~ FullToken
119
+ BlockBody.raise_missing_tag_terminator(token, parse_context)
120
+ end
121
+ tag_name = Regexp.last_match(2)
122
+ markup = Regexp.last_match(4)
78
123
 
79
- begin
80
- # If we get an Interrupt that means the block must stop processing. An
81
- # Interrupt is any command that stops block execution such as {% break %}
82
- # or {% continue %}
83
- if token.is_a?(Continue) or token.is_a?(Break)
84
- context.push_interrupt(token.interrupt)
85
- break
124
+ if parse_context.line_number
125
+ # newlines inside the tag should increase the line number,
126
+ # particularly important for multiline {% liquid %} tags
127
+ parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
128
+ end
129
+
130
+ if tag_name == 'liquid'
131
+ parse_liquid_tag(markup, parse_context)
132
+ next
133
+ end
134
+
135
+ unless (tag = registered_tags[tag_name])
136
+ # end parsing if we reach an unknown tag and let the caller decide
137
+ # determine how to proceed
138
+ return yield tag_name, markup
139
+ end
140
+ new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
141
+ @blank &&= new_tag.blank?
142
+ @nodelist << new_tag
143
+ when token.start_with?(VARSTART)
144
+ whitespace_handler(token, parse_context)
145
+ @nodelist << create_variable(token, parse_context)
146
+ @blank = false
147
+ else
148
+ if parse_context.trim_whitespace
149
+ token.lstrip!
86
150
  end
151
+ parse_context.trim_whitespace = false
152
+ @nodelist << token
153
+ @blank &&= token.match?(WhitespaceOrNothing)
154
+ end
155
+ parse_context.line_number = tokenizer.line_number
156
+ end
87
157
 
88
- token_output = render_token(token, context)
158
+ yield nil, nil
159
+ end
89
160
 
90
- unless token.is_a?(Block) && token.blank?
91
- output << token_output
161
+ def whitespace_handler(token, parse_context)
162
+ if token[2] == WhitespaceControl
163
+ previous_token = @nodelist.last
164
+ if previous_token.is_a?(String)
165
+ first_byte = previous_token.getbyte(0)
166
+ previous_token.rstrip!
167
+ if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
168
+ previous_token << first_byte
92
169
  end
93
- rescue MemoryError => e
94
- raise e
95
- rescue ::StandardError => e
96
- output << context.handle_error(e, token)
97
170
  end
98
171
  end
172
+ parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
173
+ end
174
+
175
+ def blank?
176
+ @blank
177
+ end
99
178
 
100
- output.join
179
+ # Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`)
180
+ # with a blank body.
181
+ #
182
+ # For example, in a conditional assignment like the following
183
+ #
184
+ # ```
185
+ # {% if size > max_size %}
186
+ # {% assign size = max_size %}
187
+ # {% endif %}
188
+ # ```
189
+ #
190
+ # we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method
191
+ # will remove them to reduce the render output size.
192
+ #
193
+ # Note that it is now preferred to use the `liquid` tag for this use case.
194
+ def remove_blank_strings
195
+ raise "remove_blank_strings only support being called on a blank block body" unless @blank
196
+ @nodelist.reject! { |node| node.instance_of?(String) }
101
197
  end
102
198
 
103
- private
199
+ def render(context)
200
+ render_to_output_buffer(context, +'')
201
+ end
202
+
203
+ def render_to_output_buffer(context, output)
204
+ freeze unless frozen?
205
+
206
+ context.resource_limits.increment_render_score(@nodelist.length)
104
207
 
105
- def render_token(token, context)
106
- token_output = (token.respond_to?(:render) ? token.render(context) : token)
107
- context.increment_used_resources(:render_length_current, token_output)
108
- if context.resource_limits_reached?
109
- context.resource_limits[:reached] = true
110
- raise MemoryError.new("Memory limits exceeded".freeze)
208
+ idx = 0
209
+ while (node = @nodelist[idx])
210
+ if node.instance_of?(String)
211
+ output << node
212
+ else
213
+ render_node(context, output, node)
214
+ # If we get an Interrupt that means the block must stop processing. An
215
+ # Interrupt is any command that stops block execution such as {% break %}
216
+ # or {% continue %}. These tags may also occur through Block or Include tags.
217
+ break if context.interrupt? # might have happened in a for-block
218
+ end
219
+ idx += 1
220
+
221
+ context.resource_limits.increment_write_score(output)
111
222
  end
112
- token_output
223
+
224
+ output
113
225
  end
114
226
 
115
- def create_variable(token, options)
116
- token.scan(ContentOfVariable) do |content|
117
- markup = token.is_a?(Token) ? token.child(content.first) : content.first
118
- return Variable.new(markup, options)
227
+ private
228
+
229
+ def render_node(context, output, node)
230
+ BlockBody.render_node(context, output, node)
231
+ end
232
+
233
+ def create_variable(token, parse_context)
234
+ if token =~ ContentOfVariable
235
+ markup = Regexp.last_match(1)
236
+ return Variable.new(markup, parse_context)
119
237
  end
120
- raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
238
+ BlockBody.raise_missing_variable_terminator(token, parse_context)
239
+ end
240
+
241
+ # @deprecated Use {.raise_missing_tag_terminator} instead
242
+ def raise_missing_tag_terminator(token, parse_context)
243
+ BlockBody.raise_missing_tag_terminator(token, parse_context)
244
+ end
245
+
246
+ # @deprecated Use {.raise_missing_variable_terminator} instead
247
+ def raise_missing_variable_terminator(token, parse_context)
248
+ BlockBody.raise_missing_variable_terminator(token, parse_context)
249
+ end
250
+
251
+ def registered_tags
252
+ Template.tags
121
253
  end
122
254
  end
123
255
  end