diff --git a/lib/liquid/block.rb b/lib/liquid/block.rb index e6db8692..6e33ca51 100644 --- a/lib/liquid/block.rb +++ b/lib/liquid/block.rb @@ -57,6 +57,10 @@ module Liquid @block_delimiter ||= "end#{block_name}" end + def format + "{% #{block_name} %}#{@body.format}{% #{block_delimiter} %}" + end + protected def parse_body(body, tokens) diff --git a/lib/liquid/block_body.rb b/lib/liquid/block_body.rb index 371e0581..83783e89 100644 --- a/lib/liquid/block_body.rb +++ b/lib/liquid/block_body.rb @@ -99,6 +99,18 @@ module Liquid output.join end + def format + @nodelist.map do |node| + if node.is_a?(Variable) + "{{ #{node.format} }}" + elsif node.is_a?(Tag) + node.format + else + node + end + end.join + end + private def render_token(token, context) diff --git a/lib/liquid/condition.rb b/lib/liquid/condition.rb index 33e829fd..87a5be89 100644 --- a/lib/liquid/condition.rb +++ b/lib/liquid/condition.rb @@ -33,6 +33,12 @@ module Liquid @right = right @child_relation = nil @child_condition = nil + + if right && right.is_a?(VariableLookup) && right.name.nil? + # Strip blank variable lookups resulting from cases like: + # {% if foo && bar %} + @right = nil + end end def evaluate(context = Context.new) @@ -70,6 +76,20 @@ module Liquid "#" end + def format + out = if operator.nil? + Expression.format(left) + else + "#{Expression.format(left)} #{operator} #{Expression.format(right)}" + end + + if @child_relation + "#{out} #{@child_relation.to_s} #{@child_condition.format}" + else + out + end + end + private def equal_variables(left, right) diff --git a/lib/liquid/expression.rb b/lib/liquid/expression.rb index 3c8a5854..c209adf8 100644 --- a/lib/liquid/expression.rb +++ b/lib/liquid/expression.rb @@ -8,6 +8,8 @@ module Liquid 'empty'.freeze => :empty? } + INVERTED_LITERALS = LITERALS.invert + def self.parse(markup) if LITERALS.key?(markup) LITERALS[markup] @@ -29,5 +31,18 @@ module Liquid end end + def self.format(value) + if INVERTED_LITERALS.key?(value) + INVERTED_LITERALS[value].dup + elsif value.is_a?(VariableLookup) || value.is_a?(RangeLookup) + value.format + elsif value.is_a?(String) + "\"#{value}\"" + elsif value.is_a?(Range) + "(#{value.to_s})" + else + value.to_s + end + end end end diff --git a/lib/liquid/range_lookup.rb b/lib/liquid/range_lookup.rb index efebd2c6..b2fd9899 100644 --- a/lib/liquid/range_lookup.rb +++ b/lib/liquid/range_lookup.rb @@ -18,5 +18,9 @@ module Liquid def evaluate(context) context.evaluate(@start_obj).to_i..context.evaluate(@end_obj).to_i end + + def format + "(#{Expression.format(@start_obj)}..#{Expression.format(@end_obj)})" + end end end diff --git a/lib/liquid/tag.rb b/lib/liquid/tag.rb index d8d35c56..c949b2eb 100644 --- a/lib/liquid/tag.rb +++ b/lib/liquid/tag.rb @@ -38,5 +38,13 @@ module Liquid def blank? false end + + def format + tag = @tag_name.strip + markup = @markup.strip + tag << " #{markup}" unless markup.empty? + + "{% #{tag} %}" + end end end diff --git a/lib/liquid/tags/assign.rb b/lib/liquid/tags/assign.rb index f1dfb195..417ea528 100644 --- a/lib/liquid/tags/assign.rb +++ b/lib/liquid/tags/assign.rb @@ -35,6 +35,10 @@ module Liquid def blank? true end + + def format + "{% assign #{@to} = #{@from.format} %}" + end end Template.register_tag('assign'.freeze, Assign) diff --git a/lib/liquid/tags/capture.rb b/lib/liquid/tags/capture.rb index 86743567..a13a87c0 100644 --- a/lib/liquid/tags/capture.rb +++ b/lib/liquid/tags/capture.rb @@ -32,6 +32,10 @@ module Liquid def blank? true end + + def format + "{% #{block_name} #{@to} %}#{@body.format}{% #{block_delimiter} %}" + end end Template.register_tag('capture'.freeze, Capture) diff --git a/lib/liquid/tags/case.rb b/lib/liquid/tags/case.rb index dea0be20..5148fee1 100644 --- a/lib/liquid/tags/case.rb +++ b/lib/liquid/tags/case.rb @@ -15,7 +15,7 @@ module Liquid end def parse(tokens) - body = BlockBody.new + @initial_body = body = BlockBody.new while more = parse_body(body, tokens) body = @blocks.last.attachment end @@ -53,6 +53,21 @@ module Liquid end end + def format + out = "{% case #{Expression.format(@left)} %}#{@initial_body.format}" + + @blocks.each do |block| + if block.else? + out << "{% else %}" + else + out << "{% when #{Expression.format(block.right)} %}" + end + out << block.attachment.format + end + + out + "{% endcase %}" + end + private def record_when_condition(markup) diff --git a/lib/liquid/tags/cycle.rb b/lib/liquid/tags/cycle.rb index 82ba9fb2..e6f9aa84 100644 --- a/lib/liquid/tags/cycle.rb +++ b/lib/liquid/tags/cycle.rb @@ -21,6 +21,7 @@ module Liquid when NamedSyntax @variables = variables_from_string($2) @name = Expression.parse($1) + @named = true when SimpleSyntax @variables = variables_from_string(markup) @name = @variables.to_s @@ -43,6 +44,15 @@ module Liquid end end + def format + formatted_vars = @variables.map { |v| Expression.format(v) }.join(', ') + if @named + "{% cycle #{Expression.format(@name)}: #{formatted_vars} %}" + else + "{% cycle #{formatted_vars} %}" + end + end + private def variables_from_string(markup) diff --git a/lib/liquid/tags/for.rb b/lib/liquid/tags/for.rb index 33f12af3..35d5061a 100644 --- a/lib/liquid/tags/for.rb +++ b/lib/liquid/tags/for.rb @@ -129,6 +129,16 @@ module Liquid result end + def format + out = "{% for #{@variable_name} in #{Expression.format(@collection_name)} %}" + out << @for_block.format + if @else_block + out << "{% else %}" + out << @else_block.format + end + out + "{% endfor %}" + end + protected def lax_parse(markup) diff --git a/lib/liquid/tags/if.rb b/lib/liquid/tags/if.rb index ba668f23..e4b21726 100644 --- a/lib/liquid/tags/if.rb +++ b/lib/liquid/tags/if.rb @@ -48,6 +48,25 @@ module Liquid end end + def format + out = "" + first_condition = true + + @blocks.each do |block| + if block.else? + out << "{% else %}" + elsif first_condition + out << "{% #{block_name} #{block.format} %}" + first_condition = false + else + out << "{% elsif #{block.format} %}" + end + out << block.attachment.format + end + + out + "{% #{block_delimiter} %}" + end + private def push_block(tag, markup) diff --git a/lib/liquid/tags/include.rb b/lib/liquid/tags/include.rb index b5525597..8f6dbe82 100644 --- a/lib/liquid/tags/include.rb +++ b/lib/liquid/tags/include.rb @@ -25,6 +25,7 @@ module Liquid template_name = $1 variable_name = $3 + @has_with_clause = !variable_name.nil? @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil @template_name_expr = Expression.parse(template_name) @attributes = {} @@ -70,6 +71,21 @@ module Liquid end end + def format + segments = [@tag_name, Expression.format(@template_name)] + + if @has_with_clause + segments << "with" + segments << Expression.format(@variable_name) + end + + unless @attributes.empty? + segments << @attributes.map { |k, v| "#{k}: #{Expression.format(v)}" }.join(", ") + end + + "{% #{segments.join(' ')} %}" + end + private def load_cached_partial(context) cached_partials = context.registers[:cached_partials] || {} diff --git a/lib/liquid/tags/raw.rb b/lib/liquid/tags/raw.rb index 41b2ec45..1daa2c0f 100644 --- a/lib/liquid/tags/raw.rb +++ b/lib/liquid/tags/raw.rb @@ -24,6 +24,10 @@ module Liquid def blank? @body.empty? end + + def format + "{% #{block_name} %}#{@body}{% #{block_delimiter} %}" + end end Template.register_tag('raw'.freeze, Raw) diff --git a/lib/liquid/tags/table_row.rb b/lib/liquid/tags/table_row.rb index 85b1f5aa..3025fa24 100644 --- a/lib/liquid/tags/table_row.rb +++ b/lib/liquid/tags/table_row.rb @@ -66,6 +66,12 @@ module Liquid result << "\n" result end + + def format + tag_main = "#{block_name} #{@variable_name} in #{Expression.format(@collection_name)}" + args = @attributes.map { |k, v| "#{k}: #{Expression.format(v)}" }.join(", ") + "{% #{tag_main} #{args} %}#{@body.format}{% #{block_delimiter} %}" + end end Template.register_tag('tablerow'.freeze, TableRow) diff --git a/lib/liquid/variable.rb b/lib/liquid/variable.rb index 92b0537b..1d38a318 100644 --- a/lib/liquid/variable.rb +++ b/lib/liquid/variable.rb @@ -28,6 +28,28 @@ module Liquid @markup end + def format + out = Expression.format(@name) + + @filters.each do |filter| + out << " | " + out << filter[0] + + args = filter[1].map { |arg| Expression.format(arg) } + + if filter.size > 2 + args += filter[2].map { |key, arg| "#{key}: #{Expression.format(arg)}" } + end + + unless args.empty? + out << ": " + out << args.join(', ') + end + end + + out + end + def markup_context(markup) "in \"{{#{markup}}}\"" end diff --git a/lib/liquid/variable_lookup.rb b/lib/liquid/variable_lookup.rb index 92f57c83..7a4f6b23 100644 --- a/lib/liquid/variable_lookup.rb +++ b/lib/liquid/variable_lookup.rb @@ -14,6 +14,7 @@ module Liquid name = lookups.shift if name =~ SQUARE_BRACKETED + @name_expression = true name = Expression.parse($1) end @name = name @@ -71,6 +72,20 @@ module Liquid self.class == other.class && self.state == other.state end + def format + out = @name_expression ? Expression.format(@name) : @name.dup + + @lookups.each do |lookup| + if lookup.is_a?(String) && lookup =~ /^#{VariableSegment}+$/ + out << ".#{lookup}" + else + out << "[#{Expression.format(lookup)}]" + end + end + + out + end + protected def state diff --git a/test/integration/formatter_test.rb b/test/integration/formatter_test.rb new file mode 100644 index 00000000..00b1ff49 --- /dev/null +++ b/test/integration/formatter_test.rb @@ -0,0 +1,172 @@ +require 'test_helper' + +class FormatterTest < Minitest::Test + def assert_format(expected, src) + with_error_mode(:lax) do + src_ast = Liquid::Template.parse(src).root + assert_equal expected, src_ast.format + + fmt_ast = Liquid::Template.parse(src_ast.format).root + assert_equal expected, fmt_ast.format + end + end + + def test_filters + assert_format '{{ a | b: foo, c: "foo" }}', '{{a|b:foo,c:"foo"}}' + assert_format '{{ page.attribs.title | downcase }}', "{{page.attribs['title' ]| downcase}}" + assert_format '{{ page.attribs["t.i.t.l.e"] | downcase }}', "{{page.attribs['t.i.t.l.e'] | downcase }}" + assert_format '{{ page.attribs["t&tle"] | downcase }}', "{{page.attribs['t&tle'] | downcase }}" + end + + def test_conditionals + src = """ + {% if true && !!%} + cats + {% elsif a or (b and c) && d%} + dogs + {% endif %} + + {%unless something%} + cats + {% endunless%} + """ + + expected = """ + {% if true %} + cats + {% elsif a or b and c %} + dogs + {% endif %} + + {% unless something %} + cats + {% endunless %} + """ + + assert_format expected, src + + src = """ + {%case var asdf $$#$ %} + {% when true%} + w + {% else%} + e + {%endcase %} + """ + + expected = """ + {% case var %} + {% when true %} + w + {% else %} + e + {% endcase %} + """ + + assert_format expected, src + end + + def test_comments + assert_format "{% comment %} hunter2 {% endcomment %}", "{%comment %} hunter2 {% endcomment ^ %}" + end + + def test_assigns + assert_format '{% assign foo = "monkey" %}', "{%assign foo ='monkey' ^ %}" + end + + def test_looping + src = """ + {% for i in (1..10) %} + cat + {%ifchanged%}{{i}}{% endifchanged %} + {% continue%} + {% else %} + dog + {%break %} + {% endfor %} + """ + + expected = """ + {% for i in (1..10) %} + cat + {% ifchanged %}{{ i }}{% endifchanged %} + {% continue %} + {% else %} + dog + {% break %} + {% endfor %} + """ + + assert_format expected, src + + src = "{% tablerow n in numbers cols:3 offset : 1 limit:6%} {{n}} {% endtablerow %}" + expected = "{% tablerow n in numbers cols: 3, offset: 1, limit: 6 %} {{ n }} {% endtablerow %}" + assert_format expected, src + end + + def test_capture + assert_format "{% capture foo %} foo {% endcapture %}", "{%capture foo %} foo {%endcapture%}" + end + + def test_cycle + assert_format '{% cycle "red", 2.8, "green", 1 %}', "{% cycle 'red',2.8,'green',1 %}" + end + + def test_augment + assert_format "{% decrement foo %}{% increment foo %}", "{% decrement foo%}{%increment foo %}" + end + + def test_raw + assert_format "{% raw %} foo {% endraw %}", "{%raw !!%} foo {%endraw foo%}" + end + + def test_include + src = <<-eof + {% include 'foo' %} + {% include 'foo' !!! why! %} + {% include 'foo' with bar %} + {% include 'foo' with bar baz: z qux:f %} + {% include 'foo' baz: z qux:f %} + eof + + expected = <<-eof + {% include "foo" %} + {% include "foo" %} + {% include "foo" with bar %} + {% include "foo" with bar baz: z, qux: f %} + {% include "foo" baz: z, qux: f %} + eof + + assert_format expected, src + end + + def test_quirks + src = <<-eof + {% if a == 'foo' or (b == 'bar' and c == 'baz') or false %} YES {% endif %} + {% if true && false %} YES {% endif %} + {% if false || true %} YES {% endif %} + {{ 'hi there' | split$$$:' ' | first }}"" + {{ 'X' | downcase) }} + {{ 'hi there' | split:"t"" | reverse | first}} + {{ 'hi there' | split:"t"" | remove:"i" | first}} + {% for i in (1...5) %}{{ i }}{% endfor %} + {{test |a|b|}} + {{|test|}} + eof + + expected = <<-eof + {% if a == "foo" or b == "bar" and c == "baz" or false %} YES {% endif %} + {% if true %} YES {% endif %} + {% if false %} YES {% endif %} + {{ "hi there" | split: " " | first }}"" + {{ "X" | downcase }} + {{ "hi there" | split: "t" | reverse | first }} + {{ "hi there" | split: "t" | first }} + {% for i in (1..5) %}{{ i }}{% endfor %} + {{ test | a | b }} + {{ test }} + eof + + assert_format expected, src + end +end diff --git a/test/integration/tags/table_row_test.rb b/test/integration/tags/table_row_test.rb index 3751963a..16734429 100644 --- a/test/integration/tags/table_row_test.rb +++ b/test/integration/tags/table_row_test.rb @@ -57,7 +57,7 @@ class TableRowTest < Minitest::Test def test_offset_and_limit assert_template_result("\n 1 2 3 \n 4 5 6 \n", - '{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}', + '{% tablerow n in numbers cols:3, offset:1, limit:6%} {{n}} {% endtablerow %}', 'numbers' => [0,1,2,3,4,5,6,7]) end end