diff --git a/lib/liquid.rb b/lib/liquid.rb index 0a970005..f63d160b 100644 --- a/lib/liquid.rb +++ b/lib/liquid.rb @@ -26,7 +26,9 @@ module Liquid ArgumentSeparator = ',' FilterArgumentSeparator = ':' VariableAttributeSeparator = '.' - WhitespaceControl = '-' + WhitespaceControl = '(?:[-~])' + WhitespaceTrim = '-' + WhitespaceTrimIndent = '~' TagStart = /\{\%/ TagEnd = /\%\}/ TagName = /#|\w+/ diff --git a/lib/liquid/block_body.rb b/lib/liquid/block_body.rb index e4ada7d1..a37acc29 100644 --- a/lib/liquid/block_body.rb +++ b/lib/liquid/block_body.rb @@ -164,6 +164,7 @@ module Liquid token.lstrip! end parse_context.trim_whitespace = false + parse_context.strip_trailing = false @nodelist << token @blank &&= token.match?(WhitespaceOrNothing) end @@ -174,7 +175,8 @@ module Liquid end def whitespace_handler(token, parse_context) - if token[2] == WhitespaceControl + parse_context.indentation = nil + if token[2] == WhitespaceTrim previous_token = @nodelist.last if previous_token.is_a?(String) first_byte = previous_token.getbyte(0) @@ -183,8 +185,15 @@ module Liquid previous_token << first_byte end end + elsif token[2] == WhitespaceTrimIndent + previous_token = @nodelist.last + if previous_token.is_a?(String) + start_of_line = previous_token.rindex("\n") || 0 + parse_context.indentation = previous_token[start_of_line + 1..] + end end - parse_context.trim_whitespace = (token[-3] == WhitespaceControl) + parse_context.trim_whitespace = (token[-3] == WhitespaceTrim) + parse_context.strip_trailing = (token[-3] == WhitespaceTrimIndent) end def blank? @@ -248,7 +257,7 @@ module Liquid def create_variable(token, parse_context) if token.end_with?("}}") i = 2 - i = 3 if token[i] == "-" + i = 3 if token[i] == "-" or token[i] == "~" parse_end = token.length - 3 parse_end -= 1 if token[parse_end] == "-" markup_end = parse_end - i + 1 diff --git a/lib/liquid/parse_context.rb b/lib/liquid/parse_context.rb index 7bd5418d..9bd767d3 100644 --- a/lib/liquid/parse_context.rb +++ b/lib/liquid/parse_context.rb @@ -2,7 +2,7 @@ module Liquid class ParseContext - attr_accessor :locale, :line_number, :trim_whitespace, :depth + attr_accessor :locale, :line_number, :trim_whitespace, :depth, :indentation, :strip_trailing attr_reader :partial, :warnings, :error_mode, :environment def initialize(options = Const::EMPTY_HASH) diff --git a/lib/liquid/tags/comment.rb b/lib/liquid/tags/comment.rb index 659108e3..a486eed5 100644 --- a/lib/liquid/tags/comment.rb +++ b/lib/liquid/tags/comment.rb @@ -63,10 +63,11 @@ module Liquid comment_tag_depth -= 1 end - if comment_tag_depth.zero? - parse_context.trim_whitespace = (token[-3] == WhitespaceControl) unless tokenizer.for_liquid_tag - return false - end + next unless comment_tag_depth.zero? + + parse_context.trim_whitespace = (token[-3] == WhitespaceTrim) unless tokenizer.for_liquid_tag + parse_context.strip_trailing = (token[-3] == WhitespaceTrimIndent) unless tokenizer.for_liquid_tag + return false end raise_tag_never_closed(block_name) diff --git a/lib/liquid/tags/include.rb b/lib/liquid/tags/include.rb index b4f1be13..4c170ca9 100644 --- a/lib/liquid/tags/include.rb +++ b/lib/liquid/tags/include.rb @@ -37,6 +37,7 @@ module Liquid @variable_name_expr = variable_name ? parse_expression(variable_name) : nil @template_name_expr = parse_expression(template_name) @attributes = {} + @indentation = options.indentation markup.scan(TagAttributes) do |key, value| @attributes[key] = parse_expression(value) @@ -80,15 +81,25 @@ module Liquid context[key] = context.evaluate(value) end + partial_output = +'' if variable.is_a?(Array) variable.each do |var| context[context_variable_name] = var - partial.render_to_output_buffer(context, output) + partial.render_to_output_buffer(context, partial_output) end else context[context_variable_name] = variable - partial.render_to_output_buffer(context, output) + partial.render_to_output_buffer(context, partial_output) end + + if @indentation + partial_output = partial_output.lines.map.with_index do |line, i| + next line if i == 0 + @indentation + line + end.join + end + + output << partial_output end ensure context.template_name = old_template_name diff --git a/lib/liquid/tags/raw.rb b/lib/liquid/tags/raw.rb index 2721c4a2..4eb5355b 100644 --- a/lib/liquid/tags/raw.rb +++ b/lib/liquid/tags/raw.rb @@ -25,7 +25,8 @@ module Liquid @body = +'' while (token = tokens.shift) if token =~ BlockBody::FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2) - parse_context.trim_whitespace = (token[-3] == WhitespaceControl) + parse_context.trim_whitespace = (token[-3] == WhitespaceTrim) + parse_context.strip_trailing = (token[-3] == WhitespaceTrimIndent) @body << Regexp.last_match(1) if Regexp.last_match(1) != "" return end diff --git a/lib/liquid/tags/render.rb b/lib/liquid/tags/render.rb index 3615b1b3..7d927335 100644 --- a/lib/liquid/tags/render.rb +++ b/lib/liquid/tags/render.rb @@ -46,6 +46,8 @@ module Liquid @variable_name_expr = variable_name ? parse_expression(variable_name) : nil @template_name_expr = parse_expression(template_name) @is_for_loop = (with_or_for == FOR) + @indentation = options.indentation + @strip_trailing = options.strip_trailing @attributes = {} markup.scan(TagAttributes) do |key, value| @@ -84,7 +86,18 @@ module Liquid inner_context[key] = context.evaluate(value) end inner_context[context_variable_name] = var unless var.nil? - partial.render_to_output_buffer(inner_context, output) + partial_output = +'' + partial.render_to_output_buffer(inner_context, partial_output) + if @indentation + partial_output = partial_output.lines.map.with_index do |line, i| + next line if i == 0 + @indentation + line + end.join + end + if @strip_trailing + partial_output.rstrip! + end + output << partial_output forloop&.send(:increment!) } diff --git a/lib/liquid/variable.rb b/lib/liquid/variable.rb index 372ee4db..f537efac 100644 --- a/lib/liquid/variable.rb +++ b/lib/liquid/variable.rb @@ -29,6 +29,8 @@ module Liquid @name = nil @parse_context = parse_context @line_number = parse_context.line_number + @indentation = parse_context.indentation + @strip_trailing = parse_context.strip_trailing strict_parse_with_error_mode_fallback(markup) end @@ -96,13 +98,27 @@ module Liquid def render_to_output_buffer(context, output) obj = render(context) + obj_output = '' if obj.is_a?(Array) - output << obj.join + obj_output = obj.join elsif obj.nil? else - output << obj.to_s + obj_output = obj.to_s end + if @indentation + obj_output = obj_output.lines.map.with_index do |line, i| + next line if i == 0 + @indentation + line + end.join + end + + if @strip_trailing + obj_output.rstrip! + end + + output << obj_output + output end diff --git a/test/integration/trim_mode_test.rb b/test/integration/trim_mode_test.rb index a0a59089..7497a1a3 100644 --- a/test/integration/trim_mode_test.rb +++ b/test/integration/trim_mode_test.rb @@ -555,4 +555,139 @@ class TrimModeTest < Minitest::Test def test_trim_blank assert_template_result('foobar', 'foo {{-}} bar') end + + def test_trim_indent_variable + text = <<-END_TEMPLATE +
+

+ {{~ 'Hello\nWorld' ~}} +

+
+ END_TEMPLATE + expected = <<-END_EXPECTED +
+

+ Hello + World +

+
+ END_EXPECTED + assert_template_result(expected, text) + end + + def test_trim_indent_variable_trims_trailing_whitespace + text = <<-END_TEMPLATE +
+

+ {{~ 'Hello\nWorld\n' ~}} +

+
+ END_TEMPLATE + expected = <<-END_EXPECTED +
+

+ Hello + World +

+
+ END_EXPECTED + assert_template_result(expected, text) + end + + + def test_trim_indent_tags + text = <<-END_TEMPLATE +
+

+ {%~ echo 'Hello\nWorld' ~%} +

+
+ END_TEMPLATE + expected = <<-END_EXPECTED +
+

+ Hello + World +

+
+ END_EXPECTED + assert_template_result(expected, text) + end + + def test_trim_indent_include + text = <<-END_TEMPLATE +
+

+ {%~ include "snippet" ~%} +

+
+ END_TEMPLATE + expected = <<-END_EXPECTED +
+

+ Hello + World +

+
+ END_EXPECTED + assert_template_result(expected, text, partials: {"snippet" => "Hello\nWorld"}) + end + + def test_trim_indent_render + text = <<-END_TEMPLATE +
+

+ {%~ render "snippet" ~%} +

+
+ END_TEMPLATE + expected = <<-END_EXPECTED +
+

+ Hello + World +

+
+ END_EXPECTED + assert_template_result(expected, text, partials: {"snippet" => "Hello\nWorld"}) + end + + def test_trim_indent_render_trim_trailing_whitespace + text = <<-END_TEMPLATE +
+

+ {%~ render "snippet" ~%} +

+
+ END_TEMPLATE + expected = <<-END_EXPECTED +
+

+ Hello + World +

+
+ END_EXPECTED + assert_template_result(expected, text, partials: {"snippet" => "Hello\nWorld\n"}) + end + + def test_trim_indent_nested_render + text = <<-END_TEMPLATE +
+

+ {%~ render "snippet" ~%} +

+
+ END_TEMPLATE + expected = <<-END_EXPECTED +
+

+ Hello + inside + World +

+
+ END_EXPECTED + assert_template_result(expected, text, partials: {"snippet" => "Hello\n {%~ render \"snippet2\" ~%}\nWorld", "snippet2" => "inside"}) + end end # TrimModeTest