1
0
Fork 0
mirror of https://github.com/haml/haml.git synced 2022-11-09 12:33:31 -05:00

Add an > operator to get rid of whitespace outside of a tag.

This also lays the groundwork for a sister < operator that will remove whitespace inside a tag.
This commit is contained in:
Nathan Weizenbaum 2008-05-10 00:26:19 -07:00
parent e5dedfa6a5
commit 5834545b3e
8 changed files with 436 additions and 56 deletions

View file

@ -342,6 +342,61 @@ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir)
# </div> # </div>
# </div> # </div>
# #
# ==== > and <
#
# <tt>></tt> and <tt><</tt> give you more control over the whitespace near a tag.
# <tt>></tt> will remove all whitespace surrounding a tag,
# while <tt><</tt> will remove all whitespace immediately within a tag.
# You can think of them as alligators eating the whitespace:
# <tt>></tt> faces out of the tag and eats the whitespace on the outside,
# and <tt><</tt> faces into the tag and eats the whitespace on the inside.
# They're placed at the end of a tag definition,
# after class, id, and attribute declarations
# but before <tt>/</tt> or <tt>=</tt>.
# For example:
#
# %blockquote<
# %div
# Foo!
#
# is compiled to:
#
# <blockquote><div>
# Foo!
# </div></blockquote>
#
# And:
#
# %img
# %img>
# %img
#
# is compiled to:
#
# <img /><img /><img />
#
# And:
#
# %p<= "Foo\nBar"
#
# is compiled to:
#
# <p>Foo
# Bar</p>
#
# And finally:
#
# %img
# %pre><
# foo
# bar
# %img
#
# is compiled to:
#
# <img /><pre>foo
# bar</pre><img />
#
# ==== = # ==== =
# #
# <tt>=</tt> is placed at the end of a tag definition, # <tt>=</tt> is placed at the end of a tag definition,

View file

@ -84,19 +84,23 @@ module Haml
# Renders +text+ with the proper tabulation. This also deals with # Renders +text+ with the proper tabulation. This also deals with
# making a possible one-line tag one line or not. # making a possible one-line tag one line or not.
def push_text(text, tab_change = 0) def push_text(text, dont_tab_up = false, tab_change = 0)
if @tabulation > 0 && !@options[:ugly] if @tabulation > 0 && !@options[:ugly]
# Have to push every line in by the extra user set tabulation # Have to push every line in by the extra user set tabulation.
text.gsub!(/^/m, tabs) # Don't push lines with just whitespace, though,
# because that screws up precompiled indentation.
text.gsub!(/^(?!\s+$)/m, tabs)
text.sub!(tabs, '') if dont_tab_up
end end
@buffer << text @buffer << text
@real_tabs += tab_change @real_tabs += tab_change
@dont_tab_up_next_line = false
end end
# Properly formats the output of a script that was run in the # Properly formats the output of a script that was run in the
# instance_eval. # instance_eval.
def push_script(result, preserve_script, close_tag = nil, preserve_tag = false, escape_html = false) def push_script(result, preserve_script, in_tag = false, preserve_tag = false, escape_html = false)
tabulation = @real_tabs tabulation = @real_tabs
if preserve_tag if preserve_tag
@ -114,39 +118,41 @@ module Haml
result = html_escape(result) if escape_html result = html_escape(result) if escape_html
has_newline = result.include?("\n") has_newline = result.include?("\n")
if close_tag && (@options[:ugly] || !has_newline || preserve_tag) if in_tag && (@options[:ugly] || !has_newline || preserve_tag)
@buffer << "#{result}</#{close_tag}>\n" @buffer << result
@real_tabs -= 1 @real_tabs -= 1
else return
if close_tag end
@buffer << "\n"
end
# Precompiled tabulation may be wrong if in_tag
if @tabulation > 0 && !close_tag @buffer << "\n"
result = tabs + result end
end
if has_newline && !@options[:ugly] # Precompiled tabulation may be wrong
result = result.gsub "\n", "\n" + tabs(tabulation) if @tabulation > 0 && !in_tag
result = tabs + result
end
# Add tabulation if it wasn't precompiled if has_newline && !@options[:ugly]
result = tabs(tabulation) + result if close_tag result = result.gsub "\n", "\n" + tabs(tabulation)
end
@buffer << "#{result}\n"
if close_tag # Add tabulation if it wasn't precompiled
# We never get here if @options[:ugly] is true result = tabs(tabulation) + result if in_tag
@buffer << "#{tabs(tabulation-1)}</#{close_tag}>\n" end
@real_tabs -= 1 @buffer << "#{result}\n"
end
if in_tag
# We never get here if @options[:ugly] is true
@buffer << tabs(tabulation-1)
@real_tabs -= 1
end end
nil nil
end end
# Takes the various information about the opening tag for an # Takes the various information about the opening tag for an
# element, formats it, and adds it to the buffer. # element, formats it, and adds it to the buffer.
def open_tag(name, self_closing, try_one_line, preserve_tag, escape_html, class_id, obj_ref, content, *attributes_hashes) def open_tag(name, self_closing, try_one_line, preserve_tag, escape_html, class_id,
nuke_outer_whitespace, nuke_inner_whitespace, obj_ref, content, *attributes_hashes)
tabulation = @real_tabs tabulation = @real_tabs
attributes = class_id attributes = class_id
@ -157,7 +163,7 @@ module Haml
self.class.merge_attrs(attributes, parse_object_ref(obj_ref)) if obj_ref self.class.merge_attrs(attributes, parse_object_ref(obj_ref)) if obj_ref
if self_closing if self_closing
str = " />\n" str = " />" + (nuke_outer_whitespace ? "" : "\n")
elsif try_one_line || preserve_tag elsif try_one_line || preserve_tag
str = ">" str = ">"
else else
@ -165,10 +171,10 @@ module Haml
end end
attributes = Precompiler.build_attributes(html?, @options[:attr_wrapper], attributes) attributes = Precompiler.build_attributes(html?, @options[:attr_wrapper], attributes)
@buffer << "#{@options[:ugly] ? '' : tabs(tabulation)}<#{name}#{attributes}#{str}" @buffer << "#{nuke_outer_whitespace || @options[:ugly] ? '' : tabs(tabulation)}<#{name}#{attributes}#{str}"
if content if content
@buffer << "#{content}</#{name}>\n" @buffer << "#{content}</#{name}>" << (nuke_outer_whitespace ? "" : "\n")
elsif !self_closing elsif !self_closing
@real_tabs += 1 @real_tabs += 1
end end

View file

@ -199,12 +199,12 @@ END
when ELEMENT; render_tag(text) when ELEMENT; render_tag(text)
when COMMENT; render_comment(text[1..-1].strip) when COMMENT; render_comment(text[1..-1].strip)
when SANITIZE when SANITIZE
return push_script(unescape_interpolation(text[3..-1].strip), false, nil, false, true) if text[1..2] == "==" return push_script(unescape_interpolation(text[3..-1].strip), false, false, false, true) if text[1..2] == "=="
return push_script(text[2..-1].strip, false, nil, false, true) if text[1] == SCRIPT return push_script(text[2..-1].strip, false, false, false, true) if text[1] == SCRIPT
push_plain text push_plain text
when SCRIPT when SCRIPT
return push_script(unescape_interpolation(text[2..-1].strip), false) if text[1] == SCRIPT return push_script(unescape_interpolation(text[2..-1].strip), false) if text[1] == SCRIPT
return push_script(text[1..-1], false, nil, false, true) if options[:escape_html] return push_script(text[1..-1], false, false, false, true) if options[:escape_html]
push_script(text[1..-1], false) push_script(text[1..-1], false)
when FLAT_SCRIPT; push_flat_script(text[1..-1]) when FLAT_SCRIPT; push_flat_script(text[1..-1])
when SILENT_SCRIPT when SILENT_SCRIPT
@ -277,8 +277,9 @@ END
# Adds <tt>text</tt> to <tt>@buffer</tt> with appropriate tabulation # Adds <tt>text</tt> to <tt>@buffer</tt> with appropriate tabulation
# without parsing it. # without parsing it.
def push_merged_text(text, tab_change = 0) def push_merged_text(text, tab_change = 0, indent = true)
@merged_text << (@options[:ugly] ? text : "#{' ' * @output_tabs}#{text}") @merged_text << (!indent || @dont_indent_next_line || @options[:ugly] ? text : "#{' ' * @output_tabs}#{text}")
@dont_indent_next_line = false
@tab_change += tab_change @tab_change += tab_change
end end
@ -295,9 +296,11 @@ END
return if @merged_text.empty? return if @merged_text.empty?
@precompiled << "_hamlout.push_text(#{@merged_text.dump}" @precompiled << "_hamlout.push_text(#{@merged_text.dump}"
@precompiled << ", #{@dont_tab_up_next_text.inspect}" if @dont_tab_up_next_text || @tab_change != 0
@precompiled << ", #{@tab_change}" if @tab_change != 0 @precompiled << ", #{@tab_change}" if @tab_change != 0
@precompiled << ");" @precompiled << ");"
@merged_text = '' @merged_text = ''
@dont_tab_up_next_text = false
@tab_change = 0 @tab_change = 0
end end
@ -324,9 +327,9 @@ END
# #
# If <tt>preserve_script</tt> is true, Haml::Helpers#find_and_flatten is run on # If <tt>preserve_script</tt> is true, Haml::Helpers#find_and_flatten is run on
# the result before it is added to <tt>@buffer</tt> # the result before it is added to <tt>@buffer</tt>
def push_script(text, preserve_script, close_tag = nil, preserve_tag = false, escape_html = false) def push_script(text, preserve_script, in_tag = false, preserve_tag = false, escape_html = false)
# Prerender tabulation unless we're in a tag # Prerender tabulation unless we're in a tag
push_merged_text '' unless close_tag push_merged_text '' unless in_tag
flush_merged_text flush_merged_text
return if options[:suppress_eval] return if options[:suppress_eval]
@ -335,7 +338,8 @@ END
push_silent "haml_temp = #{text}" push_silent "haml_temp = #{text}"
newline_now newline_now
out = "haml_temp = _hamlout.push_script(haml_temp, #{preserve_script.inspect}, #{close_tag.inspect}, #{preserve_tag.inspect}, #{escape_html.inspect});" args = [preserve_script, in_tag, preserve_tag, escape_html].map { |a| a.inspect }.join(', ')
out = "haml_temp = _hamlout.push_script(haml_temp, #{args});"
if @block_opened if @block_opened
push_and_tabulate([:loud, out]) push_and_tabulate([:loud, out])
else else
@ -374,10 +378,12 @@ END
# Puts a line in <tt>@precompiled</tt> that will add the closing tag of # Puts a line in <tt>@precompiled</tt> that will add the closing tag of
# the most recently opened tag. # the most recently opened tag.
def close_tag(tag) def close_tag(value)
tag, nuke_outer_whitespace, nuke_inner_whitespace = value
@output_tabs -= 1 @output_tabs -= 1
@template_tabs -= 1 @template_tabs -= 1
push_text("</#{tag}>", -1) push_merged_text("</#{tag}>" + (nuke_outer_whitespace ? "" : "\n"), -1)
@dont_indent_next_line = nuke_outer_whitespace
end end
# Closes a Ruby block. # Closes a Ruby block.
@ -505,10 +511,14 @@ END
if rest if rest
object_ref, rest = balance(rest, ?[, ?]) if rest[0] == ?[ object_ref, rest = balance(rest, ?[, ?]) if rest[0] == ?[
attributes_hash, rest = parse_attributes(rest) if rest[0] == ?{ && attributes_hash.nil? attributes_hash, rest = parse_attributes(rest) if rest[0] == ?{ && attributes_hash.nil?
action, value = rest.scan(/([=\/\~&!]?)?(.*)?/)[0] nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0]
nuke_whitespace ||= ''
nuke_outer_whitespace = nuke_whitespace.include? '>'
nuke_inner_whitespace = nuke_whitespace.include? '<'
end end
value = value.to_s.strip value = value.to_s.strip
[tag_name, attributes, attributes_hash, object_ref, action, value] [tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace,
nuke_inner_whitespace, action, value]
end end
def parse_attributes(line) def parse_attributes(line)
@ -521,10 +531,21 @@ END
# Parses a line that will render as an XHTML tag, and adds the code that will # Parses a line that will render as an XHTML tag, and adds the code that will
# render that tag to <tt>@precompiled</tt>. # render that tag to <tt>@precompiled</tt>.
def render_tag(line) def render_tag(line)
tag_name, attributes, attributes_hash, object_ref, action, value = parse_tag(line) tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace,
nuke_inner_whitespace, action, value = parse_tag(line)
raise SyntaxError.new("Illegal element: classes and ids must have values.") if attributes =~ /[\.#](\.|#|\z)/ raise SyntaxError.new("Illegal element: classes and ids must have values.") if attributes =~ /[\.#](\.|#|\z)/
# Get rid of whitespace outside of the tag if we need to
if nuke_outer_whitespace
unless @merged_text.empty?
@merged_text.rstrip!
else
push_silent("_erbout.rstrip!", false)
@dont_tab_up_next_text = true
end
end
preserve_tag = options[:preserve].include?(tag_name) preserve_tag = options[:preserve].include?(tag_name)
case action case action
@ -566,29 +587,42 @@ END
tag_closed = !@block_opened && !self_closing && !parse tag_closed = !@block_opened && !self_closing && !parse
open_tag = prerender_tag(tag_name, self_closing, attributes) open_tag = prerender_tag(tag_name, self_closing, attributes)
open_tag << "#{value}</#{tag_name}>" if tag_closed if tag_closed
open_tag << "\n" unless parse open_tag << "#{value}</#{tag_name}>"
open_tag << "\n" unless nuke_outer_whitespace
else
open_tag << "\n" unless parse || (self_closing && nuke_outer_whitespace)
end
push_merged_text(open_tag, tag_closed || self_closing ? 0 : 1,
!nuke_outer_whitespace)
push_merged_text(open_tag, tag_closed || self_closing ? 0 : 1) @dont_indent_next_line = nuke_outer_whitespace && !@block_opened
return if tag_closed return if tag_closed
else else
flush_merged_text flush_merged_text
content = value.empty? || parse ? 'nil' : value.dump content = value.empty? || parse ? 'nil' : value.dump
attributes_hash = ', ' + attributes_hash if attributes_hash attributes_hash = ', ' + attributes_hash if attributes_hash
push_silent "_hamlout.open_tag(#{tag_name.inspect}, #{self_closing.inspect}, #{(!@block_opened).inspect}, #{preserve_tag.inspect}, #{escape_html.inspect}, #{attributes.inspect}, #{object_ref}, #{content}#{attributes_hash})" args = [tag_name, self_closing, !@block_opened, preserve_tag, escape_html,
attributes, nuke_outer_whitespace, nuke_inner_whitespace
].map { |v| v.inspect }.join(', ')
push_silent "_hamlout.open_tag(#{args}, #{object_ref}, #{content}#{attributes_hash})"
@dont_tab_up_next_text = @dont_indent_next_line = nuke_outer_whitespace && !@block_opened
end end
return if self_closing return if self_closing
if value.empty? if value.empty?
push_and_tabulate([:element, tag_name]) push_and_tabulate([:element, [tag_name, nuke_outer_whitespace, nuke_inner_whitespace]])
@output_tabs += 1 @output_tabs += 1
return return
end end
if parse if parse
flush_merged_text flush_merged_text
push_script(value, preserve_script, tag_name, preserve_tag, escape_html) push_script(value, preserve_script, true, preserve_tag, escape_html)
@dont_tab_up_next_text = true
concat_merged_text("</#{tag_name}>" + (nuke_outer_whitespace ? "" : "\n"))
end end
end end

View file

@ -0,0 +1,148 @@
<p>
<p><q>
Foo
</q></p>
</p>
<p>
<p><q a='2'>
Foo
</q></p>
</p>
<p>
<p><q>Foo</q></p>
</p>
<p>
<p><q a='2'>Foo</q></p>
</p>
<p>
<p><q>
Foo
</q></p>
</p>
<p>
<p><q a='2'>
Foo
</q></p>
</p>
<p>
<p><q>Foo</q></p>
</p>
<p>
<p><q a='2'>Foo</q></p>
</p>
<p>
<p><q>
Foo
Bar
</q></p>
</p>
<p>
<p><q a='2'>
Foo
Bar
</q></p>
</p>
<p>
<p><q>
Foo
Bar
</q></p>
</p>
<p>
<p><q a='2'>
Foo
Bar
</q></p>
</p>
<p>
<p>
foo<q>
Foo
</q>bar
</p>
</p>
<p>
<p>
foo<q a='2'>
Foo
</q>bar
</p>
</p>
<p>
<p>
foo<q>Foo</q>bar
</p>
</p>
<p>
<p>
foo<q a='2'>Foo</q>bar
</p>
</p>
<p>
<p>
foo<q>
Foo
</q>bar
</p>
</p>
<p>
<p>
foo<q a='2'>
Foo
</q>bar
</p>
</p>
<p>
<p>
foo<q>Foo</q>bar
</p>
</p>
<p>
<p>
foo<q a='2'>Foo</q>bar
</p>
</p>
<p>
<p>
foo<q>
Foo
Bar
</q>bar
</p>
</p>
<p>
<p>
foo<q a='2'>
Foo
Bar
</q>bar
</p>
</p>
<p>
<p>
foo<q>
Foo
Bar
</q>bar
</p>
</p>
<p>
<p>
foo<q a='2'>
Foo
Bar
</q>bar
</p>
</p>
<p>
<p><q></q></p>
</p>
<p>
<p><q /></p>
</p>
<p>
<p><q a='2'></q></p>
</p>
<p>
<p><q a='2' /></p>
</p>

View file

@ -21,7 +21,3 @@
<div class='123'>f</div> <div class='123'>f</div>
<div class='foo2u'>g</div> <div class='foo2u'>g</div>
</div> </div>
<div class='broken'>
<foo><{ :a => :b }</foo>
<div class='foo'>>{ :c => :d }</div>
</div>

View file

@ -15,7 +15,7 @@ class TemplateTest < Test::Unit::TestCase
@@templates = %w{ very_basic standard helpers @@templates = %w{ very_basic standard helpers
whitespace_handling original_engine list helpful whitespace_handling original_engine list helpful
silent_script tag_parsing just_stuff partials silent_script tag_parsing just_stuff partials
filters } filters nuke_outer_whitespace }
def setup def setup
Haml::Template.options = { :filters => { 'test'=>TestFilter } } Haml::Template.options = { :filters => { 'test'=>TestFilter } }

View file

@ -0,0 +1,144 @@
%p
%p
%q>
Foo
%p
%p
%q{:a => 1 + 1}>
Foo
%p
%p
%q> Foo
%p
%p
%q{:a => 1 + 1}> Foo
%p
%p
%q>
= "Foo"
%p
%p
%q{:a => 1 + 1}>
= "Foo"
%p
%p
%q>= "Foo"
%p
%p
%q{:a => 1 + 1}>= "Foo"
%p
%p
%q>
= "Foo\nBar"
%p
%p
%q{:a => 1 + 1}>
= "Foo\nBar"
%p
%p
%q>= "Foo\nBar"
%p
%p
%q{:a => 1 + 1}>= "Foo\nBar"
%p
%p
- tab_up
foo
%q>
Foo
bar
- tab_down
%p
%p
- tab_up
foo
%q{:a => 1 + 1}>
Foo
bar
- tab_down
%p
%p
- tab_up
foo
%q> Foo
bar
- tab_down
%p
%p
- tab_up
foo
%q{:a => 1 + 1}> Foo
bar
- tab_down
%p
%p
- tab_up
foo
%q>
= "Foo"
bar
- tab_down
%p
%p
- tab_up
foo
%q{:a => 1 + 1}>
= "Foo"
bar
- tab_down
%p
%p
- tab_up
foo
%q>= "Foo"
bar
- tab_down
%p
%p
- tab_up
foo
%q{:a => 1 + 1}>= "Foo"
bar
- tab_down
%p
%p
- tab_up
foo
%q>
= "Foo\nBar"
bar
- tab_down
%p
%p
- tab_up
foo
%q{:a => 1 + 1}>
= "Foo\nBar"
bar
- tab_down
%p
%p
- tab_up
foo
%q>= "Foo\nBar"
bar
- tab_down
%p
%p
- tab_up
foo
%q{:a => 1 + 1}>= "Foo\nBar"
bar
- tab_down
%p
%p
%q>
%p
%p
%q>/
%p
%p
%q{:a => 1 + 1}>
%p
%p
%q{:a => 1 + 1}>/

View file

@ -19,6 +19,3 @@
.foo16 e .foo16 e
.123 f .123 f
.foo2u g .foo2u g
%div.broken
%foo<{ :a => :b }
.foo>{ :c => :d }