2007-11-26 01:36:57 +00:00
require 'strscan'
2007-11-23 09:15:00 +00:00
module Haml
module Precompiler
# Designates an XHTML/XML element.
ELEMENT = ?%
# Designates a <tt><div></tt> element with the given class.
DIV_CLASS = ?.
# Designates a <tt><div></tt> element with the given id.
DIV_ID = ?#
# Designates an XHTML/XML comment.
COMMENT = ?/
2008-03-16 15:43:52 -07:00
# Designates an XHTML doctype or script that is never HTML-escaped.
2007-11-23 09:15:00 +00:00
DOCTYPE = ?!
# Designates script, the result of which is output.
SCRIPT = ?=
2008-03-16 15:43:52 -07:00
# Designates script that is always HTML-escaped.
2008-03-14 16:39:19 -07:00
SANITIZE = ?&
2007-11-23 09:15:00 +00:00
# Designates script, the result of which is flattened and output.
FLAT_SCRIPT = ?~
# Designates script which is run but not output.
SILENT_SCRIPT = ?-
# When following SILENT_SCRIPT, designates a comment that is not output.
SILENT_COMMENT = ?#
# Designates a non-parsed line.
ESCAPE = ?\\
# Designates a block of filtered text.
FILTER = ?:
# Designates a non-parsed line. Not actually a character.
PLAIN_TEXT = - 1
# Keeps track of the ASCII values of the characters that begin a
# specially-interpreted line.
SPECIAL_CHARACTERS = [
ELEMENT ,
DIV_CLASS ,
DIV_ID ,
COMMENT ,
DOCTYPE ,
SCRIPT ,
2008-03-14 16:39:19 -07:00
SANITIZE ,
2007-11-23 09:15:00 +00:00
FLAT_SCRIPT ,
SILENT_SCRIPT ,
ESCAPE ,
FILTER
]
# The value of the character that designates that a line is part
# of a multiline string.
MULTILINE_CHAR_VALUE = ?|
# Characters that designate that a multiline string may be about
# to begin.
MULTILINE_STARTERS = SPECIAL_CHARACTERS - [ ?/ ]
# Keywords that appear in the middle of a Ruby block with lowered
# indentation. If a block has been started using indentation,
# lowering the indentation with one of these won't end the block.
# For example:
#
# - if foo
# %p yes!
# - else
# %p no!
#
# The block is ended after <tt>%p no!</tt>, because <tt>else</tt>
# is a member of this array.
MID_BLOCK_KEYWORDS = [ 'else' , 'elsif' , 'rescue' , 'ensure' , 'when' ]
# The Regex that matches a Doctype command.
DOCTYPE_REGEX = / ( \ d \ . \ d)?[ \ s]*([a-z]*) /i
# The Regex that matches a literal string or symbol value
LITERAL_VALUE_REGEX = / ^ \ s*(:( \ w*)|(('|")([^ \\ \ # '"]*?) \ 4)) \ s*$ /
private
2007-11-23 17:26:05 +00:00
# Returns the precompiled string with the preamble and postamble
2007-11-24 08:35:10 +00:00
def precompiled_with_ambles ( local_names )
2007-11-24 02:32:18 +00:00
preamble = <<END.gsub("\n", ";")
2007-11-23 17:26:05 +00:00
extend Haml :: Helpers
2008-04-24 09:52:26 -07:00
_hamlout = @haml_buffer = Haml :: Buffer . new ( @haml_buffer , #{options_for_buffer.inspect})
2007-11-23 17:26:05 +00:00
_erbout = _hamlout . buffer
2007-11-24 01:29:57 +00:00
END
2007-11-24 02:32:18 +00:00
postamble = <<END.gsub("\n", ";")
2008-04-24 09:52:26 -07:00
@haml_buffer = @haml_buffer . upper
2008-04-24 12:59:03 -07:00
_erbout
2007-11-23 17:26:05 +00:00
END
2007-11-24 08:35:10 +00:00
preamble + locals_code ( local_names ) + @precompiled + postamble
end
def locals_code ( names )
names = names . keys if Hash == names
names . map do | name |
" #{ name } = _haml_locals[ #{ name . to_sym . inspect } ] || _haml_locals[ #{ name . to_s . inspect } ] "
end . join ( ';' ) + ';'
2007-11-23 17:26:05 +00:00
end
2007-11-25 19:36:06 +00:00
Line = Struct . new ( " Line " , :text , :unstripped , :index , :spaces , :tabs )
2007-11-25 18:39:34 +00:00
2007-11-23 09:15:00 +00:00
def precompile
@precompiled = ''
2007-11-25 20:49:19 +00:00
@merged_text = ''
@tab_change = 0
2007-11-23 16:32:03 +00:00
2007-11-25 19:36:06 +00:00
old_line = Line . new
2008-01-31 06:21:24 +00:00
( @template + " \n - # \n - # " ) . split ( / \ n? \ r| \ r? \ n / ) . each_with_index do | text , index |
2007-11-25 19:36:06 +00:00
line = Line . new text . strip , text . lstrip . chomp , index
line . spaces , line . tabs = count_soft_tabs ( text )
2007-11-25 18:39:34 +00:00
2007-11-25 19:36:06 +00:00
if line . text . empty?
2008-05-02 00:10:49 -07:00
process_indent ( old_line ) if flat? && ! old_line . text . empty?
2007-11-26 03:26:16 +00:00
unless flat?
newline
next
end
2007-11-25 18:39:34 +00:00
2007-11-25 19:48:37 +00:00
push_flat ( old_line )
2007-11-25 19:36:06 +00:00
old_line . text , old_line . unstripped , old_line . spaces = '' , '' , 0
2007-11-26 03:26:16 +00:00
newline
2007-11-25 19:36:06 +00:00
next
end
2007-11-25 18:39:34 +00:00
2007-11-25 19:48:37 +00:00
suppress_render = handle_multiline ( old_line ) unless flat?
2007-11-25 19:36:06 +00:00
if old_line . text . nil? || suppress_render
old_line = line
2008-05-02 00:48:39 -07:00
resolve_newlines
2007-11-26 03:26:16 +00:00
newline
2007-11-25 18:39:34 +00:00
next
end
2007-11-25 20:09:02 +00:00
process_indent ( old_line ) unless old_line . text . empty?
2007-11-23 09:15:00 +00:00
2007-11-25 19:36:06 +00:00
if flat?
2007-11-25 19:48:37 +00:00
push_flat ( old_line )
2007-11-25 19:36:06 +00:00
old_line = line
2007-11-26 03:26:16 +00:00
newline
2007-11-25 19:36:06 +00:00
next
end
2007-11-25 18:39:34 +00:00
2007-11-25 19:36:06 +00:00
if old_line . spaces != old_line . tabs * 2
2008-04-18 12:13:35 -07:00
raise SyntaxError . new ( <<END.strip, 1 + old_line.index - @index)
#{old_line.spaces} space#{old_line.spaces == 1 ? ' was' : 's were'} used for indentation. Haml must be indented using two spaces.
END
2007-11-25 19:36:06 +00:00
end
2007-11-25 18:39:34 +00:00
2007-11-25 19:36:06 +00:00
unless old_line . text . empty? || @haml_comment
process_line ( old_line . text , old_line . index , line . tabs > old_line . tabs && ! line . text . empty? )
end
2008-04-28 21:28:01 -07:00
resolve_newlines
2007-11-25 19:36:06 +00:00
if ! flat? && line . tabs - old_line . tabs > 1
2008-04-18 13:17:50 -07:00
raise SyntaxError . new ( <<END.strip, 2 + old_line.index - @index)
2008-04-18 12:13:35 -07:00
#{line.spaces} spaces were used for indentation. Haml must be indented using two spaces.
END
2007-11-23 09:15:00 +00:00
end
2007-11-25 18:39:34 +00:00
old_line = line
2007-11-26 03:26:16 +00:00
newline
2007-11-23 09:15:00 +00:00
end
# Close all the open tags
2007-11-25 19:36:06 +00:00
close until @to_close_stack . empty?
2007-11-23 16:32:03 +00:00
flush_merged_text
2007-11-23 09:15:00 +00:00
end
2008-04-07 23:09:17 -07:00
2007-11-23 09:15:00 +00:00
# Processes and deals with lowering indentation.
2007-11-25 20:09:02 +00:00
def process_indent ( line )
2007-11-25 20:22:21 +00:00
return unless line . tabs < = @template_tabs && @template_tabs > 0
2007-11-25 20:23:30 +00:00
to_close = @template_tabs - line . tabs
to_close . times { | i | close unless to_close - 1 - i == 0 && mid_block_keyword? ( line . text ) }
2007-11-23 09:15:00 +00:00
end
# Processes a single line of Haml.
#
# This method doesn't return anything; it simply processes the line and
# adds the appropriate code to <tt>@precompiled</tt>.
2007-11-25 20:20:44 +00:00
def process_line ( text , index , block_opened )
2007-11-23 09:15:00 +00:00
@block_opened = block_opened
2007-11-26 03:26:16 +00:00
@index = index + 1
2007-11-23 09:15:00 +00:00
2007-11-25 20:20:44 +00:00
case text [ 0 ]
2007-12-26 02:22:23 +00:00
when DIV_CLASS , DIV_ID ; render_div ( text )
when ELEMENT ; render_tag ( text )
2008-04-28 20:59:43 -07:00
when COMMENT ; render_comment ( text [ 1 .. - 1 ] . strip )
2008-03-16 16:53:08 +08:00
when SANITIZE
2008-05-10 00:26:19 -07:00
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 , false , false , true ) if text [ 1 ] == SCRIPT
2008-03-14 16:39:19 -07:00
push_plain text
2007-11-23 09:15:00 +00:00
when SCRIPT
2007-11-25 20:20:44 +00:00
return push_script ( unescape_interpolation ( text [ 2 .. - 1 ] . strip ) , false ) if text [ 1 ] == SCRIPT
2008-05-10 00:26:19 -07:00
return push_script ( text [ 1 .. - 1 ] , false , false , false , true ) if options [ :escape_html ]
2008-03-16 15:43:52 -07:00
push_script ( text [ 1 .. - 1 ] , false )
2007-12-26 02:22:23 +00:00
when FLAT_SCRIPT ; push_flat_script ( text [ 1 .. - 1 ] )
2007-11-23 09:15:00 +00:00
when SILENT_SCRIPT
2007-11-25 20:20:44 +00:00
return start_haml_comment if text [ 1 ] == SILENT_COMMENT
2007-11-26 03:26:16 +00:00
push_silent ( text [ 1 .. - 1 ] , true )
2008-04-28 21:28:01 -07:00
newline_now
2007-11-26 03:26:16 +00:00
if ( @block_opened && ! mid_block_keyword? ( text ) ) || text [ 1 .. - 1 ] . split ( ' ' , 2 ) [ 0 ] == " case "
2007-11-25 20:20:44 +00:00
push_and_tabulate ( [ :script ] )
2007-11-23 09:15:00 +00:00
end
2007-12-26 02:22:23 +00:00
when FILTER ; start_filtered ( text [ 1 .. - 1 ] . downcase )
2007-11-23 09:15:00 +00:00
when DOCTYPE
2007-11-25 20:20:44 +00:00
return render_doctype ( text ) if text [ 0 ... 3 ] == '!!!'
2008-03-18 02:19:41 -07:00
return push_script ( unescape_interpolation ( text [ 3 .. - 1 ] . strip ) , false ) if text [ 1 .. 2 ] == " == "
2008-03-14 16:39:19 -07:00
return push_script ( text [ 2 .. - 1 ] . strip , false ) if text [ 1 ] == SCRIPT
2007-11-25 20:20:44 +00:00
push_plain text
2007-12-26 02:22:23 +00:00
when ESCAPE ; push_plain text [ 1 .. - 1 ]
2007-11-25 20:20:44 +00:00
else push_plain text
2007-11-23 09:15:00 +00:00
end
end
2008-04-07 23:09:17 -07:00
2007-11-25 20:20:44 +00:00
# Returns whether or not the text is a silent script text with one
2007-11-23 09:15:00 +00:00
# of Ruby's mid-block keywords.
2007-11-25 20:20:44 +00:00
def mid_block_keyword? ( text )
text . length > 2 && text [ 0 ] == SILENT_SCRIPT && MID_BLOCK_KEYWORDS . include? ( text [ 1 .. - 1 ] . split [ 0 ] )
2007-11-23 09:15:00 +00:00
end
# Deals with all the logic of figuring out whether a given line is
# the beginning, continuation, or end of a multiline sequence.
#
# This returns whether or not the line should be
# rendered normally.
2007-11-25 19:48:37 +00:00
def handle_multiline ( line )
text = line . text
# A multiline string is active, and is being continued
if is_multiline? ( text ) && @multiline
@multiline . text << text [ 0 ... - 1 ]
return true
end
2008-04-07 23:09:17 -07:00
2007-11-25 19:48:37 +00:00
# A multiline string has just been activated, start adding the lines
if is_multiline? ( text ) && ( MULTILINE_STARTERS . include? text [ 0 ] )
@multiline = Line . new text [ 0 ... - 1 ] , nil , line . index , nil , line . tabs
2007-11-25 20:09:02 +00:00
process_indent ( line )
2007-11-25 19:48:37 +00:00
return true
end
# A multiline string has just ended, make line into the result
if @multiline && ! line . text . empty?
process_line ( @multiline . text , @multiline . index , line . tabs > @multiline . tabs )
@multiline = nil
2007-11-23 09:15:00 +00:00
end
2007-11-25 19:48:37 +00:00
return false
2007-11-23 09:15:00 +00:00
end
# Checks whether or not +line+ is in a multiline sequence.
2007-11-25 20:09:02 +00:00
def is_multiline? ( text )
text && text . length > 1 && text [ - 1 ] == MULTILINE_CHAR_VALUE && text [ - 2 ] == ?\s
2007-11-23 09:15:00 +00:00
end
# Evaluates <tt>text</tt> in the context of the scope object, but
# does not output the result.
2007-11-26 03:26:16 +00:00
def push_silent ( text , can_suppress = false )
2008-04-07 23:09:17 -07:00
flush_merged_text
2007-11-25 20:49:19 +00:00
return if can_suppress && options [ :suppress_eval ]
2007-11-26 03:26:16 +00:00
@precompiled << " #{ text } ; "
2007-11-23 09:15:00 +00:00
end
# Adds <tt>text</tt> to <tt>@buffer</tt> with appropriate tabulation
# without parsing it.
2008-05-10 00:26:19 -07:00
def push_merged_text ( text , tab_change = 0 , indent = true )
@merged_text << ( ! indent || @dont_indent_next_line || @options [ :ugly ] ? text : " #{ ' ' * @output_tabs } #{ text } " )
@dont_indent_next_line = false
2007-11-25 20:49:19 +00:00
@tab_change += tab_change
2007-11-23 09:15:00 +00:00
end
2008-02-22 23:03:25 -08:00
# Concatenate <tt>text</tt> to <tt>@buffer</tt> without tabulation.
def concat_merged_text ( text )
@merged_text << text
end
2008-04-07 23:09:17 -07:00
2008-05-09 16:20:28 -07:00
def push_text ( text , tab_change = 0 )
push_merged_text ( " #{ text } \n " , tab_change )
2007-11-23 09:15:00 +00:00
end
2008-04-07 23:09:17 -07:00
2007-11-23 09:15:00 +00:00
def flush_merged_text
2007-11-25 20:49:19 +00:00
return if @merged_text . empty?
@precompiled << " _hamlout.push_text( #{ @merged_text . dump } "
2008-05-10 00:26:19 -07:00
@precompiled << " , #{ @dont_tab_up_next_text . inspect } " if @dont_tab_up_next_text || @tab_change != 0
2008-04-24 19:14:52 -07:00
@precompiled << " , #{ @tab_change } " if @tab_change != 0
2007-11-26 03:26:16 +00:00
@precompiled << " ); "
2007-11-25 20:49:19 +00:00
@merged_text = ''
2008-05-10 00:26:19 -07:00
@dont_tab_up_next_text = false
2007-11-25 20:49:19 +00:00
@tab_change = 0
2008-04-07 23:09:17 -07:00
end
2007-11-23 09:15:00 +00:00
# Renders a block of text as plain text.
# Also checks for an illegally opened block.
def push_plain ( text )
2008-04-18 11:43:29 -07:00
raise SyntaxError . new ( " Illegal nesting: nesting within plain text is illegal. " , 1 ) if @block_opened
2007-11-23 09:15:00 +00:00
push_text text
end
# Adds +text+ to <tt>@buffer</tt> while flattening text.
2007-11-25 19:48:37 +00:00
def push_flat ( line )
2008-02-13 10:30:21 +01:00
unless @options [ :ugly ]
tabulation = line . spaces - @flat_spaces
tabulation = tabulation > - 1 ? tabulation : 0
@filter_buffer << " #{ ' ' * tabulation } #{ line . unstripped } \n "
else
@filter_buffer << " #{ line . unstripped } \n "
end
2007-11-23 09:15:00 +00:00
end
# Causes <tt>text</tt> to be evaluated in the context of
# the scope object and the result to be added to <tt>@buffer</tt>.
#
2008-03-02 16:12:57 -08:00
# If <tt>preserve_script</tt> is true, Haml::Helpers#find_and_flatten is run on
2007-11-23 09:15:00 +00:00
# the result before it is added to <tt>@buffer</tt>
2008-05-10 03:23:47 -07:00
def push_script ( text , preserve_script , in_tag = false , preserve_tag = false ,
escape_html = false , nuke_inner_whitespace = false )
2008-05-09 20:20:53 -07:00
# Prerender tabulation unless we're in a tag
2008-05-10 00:26:19 -07:00
push_merged_text '' unless in_tag
2008-05-09 20:20:53 -07:00
2007-11-23 09:15:00 +00:00
flush_merged_text
2007-11-25 20:49:19 +00:00
return if options [ :suppress_eval ]
2008-04-18 12:25:00 -07:00
raise SyntaxError . new ( " There's no Ruby code for = to evaluate. " ) if text . empty?
2008-04-18 08:48:46 -07:00
2007-11-26 03:26:16 +00:00
push_silent " haml_temp = #{ text } "
2008-04-28 21:28:01 -07:00
newline_now
2008-05-10 03:23:47 -07:00
args = [ preserve_script , in_tag , preserve_tag ,
escape_html , nuke_inner_whitespace ] . map { | a | a . inspect } . join ( ', ' )
2008-05-10 00:26:19 -07:00
out = " haml_temp = _hamlout.push_script(haml_temp, #{ args } ); "
2007-11-25 20:49:19 +00:00
if @block_opened
push_and_tabulate ( [ :loud , out ] )
else
@precompiled << out
2007-11-23 09:15:00 +00:00
end
end
2008-04-07 23:09:17 -07:00
2007-11-23 09:15:00 +00:00
# Causes <tt>text</tt> to be evaluated, and Haml::Helpers#find_and_flatten
# to be run on it afterwards.
def push_flat_script ( text )
flush_merged_text
2008-04-07 23:09:17 -07:00
2008-04-18 12:25:00 -07:00
raise SyntaxError . new ( " There's no Ruby code for ~ to evaluate. " ) if text . empty?
2007-11-25 20:49:19 +00:00
push_script ( text , true )
2007-11-23 09:15:00 +00:00
end
def start_haml_comment
2007-11-25 20:49:19 +00:00
return unless @block_opened
@haml_comment = true
push_and_tabulate ( [ :haml_comment ] )
2007-11-23 09:15:00 +00:00
end
# Closes the most recent item in <tt>@to_close_stack</tt>.
def close
tag , value = @to_close_stack . pop
case tag
2007-12-26 02:22:23 +00:00
when :script ; close_block
when :comment ; close_comment value
when :element ; close_tag value
when :loud ; close_loud value
when :filtered ; close_filtered value
when :haml_comment ; close_haml_comment
2007-11-23 09:15:00 +00:00
end
end
# Puts a line in <tt>@precompiled</tt> that will add the closing tag of
# the most recently opened tag.
2008-05-10 00:26:19 -07:00
def close_tag ( value )
tag , nuke_outer_whitespace , nuke_inner_whitespace = value
2008-05-10 03:23:47 -07:00
@output_tabs -= 1 unless nuke_inner_whitespace
2007-11-23 09:15:00 +00:00
@template_tabs -= 1
2008-05-10 03:23:47 -07:00
rstrip_buffer! if nuke_inner_whitespace
push_merged_text ( " </ #{ tag } > " + ( nuke_outer_whitespace ? " " : " \n " ) ,
nuke_inner_whitespace ? 0 : - 1 , ! nuke_inner_whitespace )
2008-05-10 00:26:19 -07:00
@dont_indent_next_line = nuke_outer_whitespace
2007-11-23 09:15:00 +00:00
end
# Closes a Ruby block.
def close_block
2007-11-26 03:26:16 +00:00
push_silent " end " , true
2007-11-23 09:15:00 +00:00
@template_tabs -= 1
end
# Closes a comment.
def close_comment ( has_conditional )
@output_tabs -= 1
@template_tabs -= 1
close_tag = has_conditional ? " <![endif]--> " : " --> "
push_text ( close_tag , - 1 )
end
2008-04-07 23:09:17 -07:00
2007-11-23 09:15:00 +00:00
# Closes a loud Ruby block.
def close_loud ( command )
2007-11-26 03:26:16 +00:00
push_silent 'end' , true
2007-11-23 09:15:00 +00:00
@precompiled << command
@template_tabs -= 1
end
# Closes a filtered block.
def close_filtered ( filter )
@flat_spaces = - 1
2008-02-23 01:13:36 -08:00
filter . internal_compile ( self , @filter_buffer )
2007-11-23 09:15:00 +00:00
@filter_buffer = nil
@template_tabs -= 1
end
def close_haml_comment
@haml_comment = false
@template_tabs -= 1
end
2008-04-07 23:09:17 -07:00
2007-11-23 09:15:00 +00:00
# Iterates through the classes and ids supplied through <tt>.</tt>
# and <tt>#</tt> syntax, and returns a hash with them as attributes,
# that can then be merged with another attributes hash.
def parse_class_and_id ( list )
attributes = { }
list . scan ( / ([ # .])([-_a-zA-Z0-9]+) / ) do | type , property |
case type
when '.'
if attributes [ 'class' ]
attributes [ 'class' ] += " "
else
attributes [ 'class' ] = " "
end
attributes [ 'class' ] += property
2007-12-26 02:22:23 +00:00
when '#' ; attributes [ 'id' ] = property
2007-11-23 09:15:00 +00:00
end
end
attributes
end
def parse_literal_value ( text )
2007-11-25 20:59:44 +00:00
return nil unless text
2007-11-23 09:15:00 +00:00
text . match ( LITERAL_VALUE_REGEX )
# $2 holds the value matched by a symbol, but is nil for a string match
# $5 holds the value matched by a string
$2 || $5
end
2008-04-07 23:09:17 -07:00
def parse_static_hash ( text )
2007-12-19 09:41:33 +00:00
return { } unless text
2007-11-25 20:59:44 +00:00
2007-11-23 09:15:00 +00:00
attributes = { }
2007-12-19 09:41:33 +00:00
text . split ( ',' ) . each do | attrib |
2007-11-25 20:59:44 +00:00
key , value , more = attrib . split ( '=>' )
2007-11-23 09:15:00 +00:00
2007-11-25 20:59:44 +00:00
# Make sure the key and value and only the key and value exist
2007-12-19 09:41:33 +00:00
# Otherwise, it's too complicated or dynamic and we'll defer it to the actual Ruby parser
2007-11-25 20:59:44 +00:00
key = parse_literal_value key
value = parse_literal_value value
return nil if more || key . nil? || value . nil?
attributes [ key ] = value
2007-11-23 09:15:00 +00:00
end
attributes
end
# This is a class method so it can be accessed from Buffer.
2008-03-18 16:39:56 -07:00
def self . build_attributes ( is_html , attr_wrapper , attributes = { } )
2007-11-23 09:15:00 +00:00
quote_escape = attr_wrapper == '"' ? " " " : " ' "
other_quote_char = attr_wrapper == '"' ? " ' " : '"'
2008-04-07 23:09:17 -07:00
2007-11-25 20:59:44 +00:00
result = attributes . collect do | attr , value |
next if value . nil?
2008-03-02 17:20:43 -08:00
if value == true
next " #{ attr } " if is_html
next " #{ attr } = #{ attr_wrapper } #{ attr } #{ attr_wrapper } "
elsif value == false
next
2008-02-29 17:56:38 -08:00
end
2008-03-18 16:39:56 -07:00
value = Haml :: Helpers . escape_once ( value . to_s )
2008-04-30 02:50:10 -07:00
# We want to decide whether or not to escape quotes
value . gsub! ( '"' , '"' )
2007-11-25 20:59:44 +00:00
this_attr_wrapper = attr_wrapper
if value . include? attr_wrapper
if value . include? other_quote_char
value = value . gsub ( attr_wrapper , quote_escape )
else
this_attr_wrapper = other_quote_char
2007-11-23 09:15:00 +00:00
end
end
2007-11-25 20:59:44 +00:00
" #{ attr } = #{ this_attr_wrapper } #{ value } #{ this_attr_wrapper } "
2008-03-18 16:39:56 -07:00
end
result . compact . sort . join
2007-11-23 09:15:00 +00:00
end
2008-03-18 16:39:56 -07:00
def prerender_tag ( name , self_close , attributes )
attributes_string = Precompiler . build_attributes ( html? , @options [ :attr_wrapper ] , attributes )
2008-02-26 10:57:45 -08:00
" < #{ name } #{ attributes_string } #{ self_close && xhtml? ? ' /' : '' } > "
2007-11-23 09:15:00 +00:00
end
2008-04-07 23:09:17 -07:00
2008-02-10 19:45:36 -05:00
# Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
def parse_tag ( line )
2008-04-18 11:43:29 -07:00
raise SyntaxError . new ( " Invalid tag: \" #{ line } \" . " ) unless match = line . scan ( / %([-: \ w]+)([- \ w \ . \ # ]*)(.*) / ) [ 0 ]
2008-02-10 19:45:36 -05:00
tag_name , attributes , rest = match
2008-04-27 21:14:12 -07:00
attributes_hash , rest = parse_attributes ( rest ) if rest [ 0 ] == ?{
2008-02-10 19:45:36 -05:00
if rest
object_ref , rest = balance ( rest , ?[ , ?] ) if rest [ 0 ] == ?[
2008-04-27 21:14:12 -07:00
attributes_hash , rest = parse_attributes ( rest ) if rest [ 0 ] == ?{ && attributes_hash . nil?
2008-05-10 00:26:19 -07:00
nuke_whitespace , action , value = rest . scan ( / (<>|><|[><])?([= \/ \ ~&!])?(.*)? / ) [ 0 ]
nuke_whitespace || = ''
nuke_outer_whitespace = nuke_whitespace . include? '>'
nuke_inner_whitespace = nuke_whitespace . include? '<'
2008-02-10 19:45:36 -05:00
end
value = value . to_s . strip
2008-05-10 00:26:19 -07:00
[ tag_name , attributes , attributes_hash , object_ref , nuke_outer_whitespace ,
nuke_inner_whitespace , action , value ]
2008-02-10 19:45:36 -05:00
end
2007-11-23 09:15:00 +00:00
2008-04-27 21:14:12 -07:00
def parse_attributes ( line )
scanner = StringScanner . new ( line )
attributes_hash , rest = balance ( scanner , ?{ , ?} )
attributes_hash = attributes_hash [ 1 ... - 1 ] if attributes_hash
return attributes_hash , rest
end
2007-11-23 09:15:00 +00:00
# Parses a line that will render as an XHTML tag, and adds the code that will
# render that tag to <tt>@precompiled</tt>.
def render_tag ( line )
2008-05-10 00:26:19 -07:00
tag_name , attributes , attributes_hash , object_ref , nuke_outer_whitespace ,
nuke_inner_whitespace , action , value = parse_tag ( line )
2008-04-07 23:09:17 -07:00
2007-11-25 23:56:54 +00:00
raise SyntaxError . new ( " Illegal element: classes and ids must have values. " ) if attributes =~ / [ \ . # ]( \ .| # | \ z) /
2008-05-10 00:26:19 -07:00
# Get rid of whitespace outside of the tag if we need to
2008-05-10 03:23:47 -07:00
rstrip_buffer! if nuke_outer_whitespace
2008-05-10 00:26:19 -07:00
2008-03-02 16:24:47 -08:00
preserve_tag = options [ :preserve ] . include? ( tag_name )
2008-05-11 01:39:02 -07:00
nuke_inner_whitespace || = preserve_tag
2008-03-02 16:12:57 -08:00
2007-11-25 23:56:54 +00:00
case action
2008-04-18 12:54:15 -07:00
when '/' ; self_closing = xhtml?
2008-03-02 16:12:57 -08:00
when '~' ; parse = preserve_script = true
2007-12-26 02:22:23 +00:00
when '='
2007-11-25 23:56:54 +00:00
parse = true
2007-11-26 00:36:03 +00:00
value = unescape_interpolation ( value [ 1 .. - 1 ] . strip ) if value [ 0 ] == ?=
2008-03-14 16:39:19 -07:00
when '&' , '!'
if value [ 0 ] == ?=
parse = true
2008-03-18 02:19:41 -07:00
value = ( value [ 1 ] == ?= ? unescape_interpolation ( value [ 2 .. - 1 ] . strip ) : value [ 1 .. - 1 ] . strip )
2008-03-14 16:39:19 -07:00
end
2007-11-25 23:56:54 +00:00
end
2008-03-18 02:19:41 -07:00
2007-11-25 23:56:54 +00:00
if parse && @options [ :suppress_eval ]
parse = false
value = ''
end
2007-11-23 09:15:00 +00:00
2008-03-14 16:39:19 -07:00
escape_html = ( action == '&' || ( action != '!' && @options [ :escape_html ] ) )
2007-11-25 23:56:54 +00:00
object_ref = " nil " if object_ref . nil? || @options [ :suppress_eval ]
2007-11-23 09:15:00 +00:00
2007-11-25 23:56:54 +00:00
static_attributes = parse_static_hash ( attributes_hash ) # Try pre-compiling a static attributes hash
2007-12-19 09:41:33 +00:00
attributes_hash = nil if static_attributes || @options [ :suppress_eval ]
2007-11-25 23:56:54 +00:00
attributes = parse_class_and_id ( attributes )
Buffer . merge_attrs ( attributes , static_attributes ) if static_attributes
2007-11-23 09:15:00 +00:00
2008-04-18 12:54:15 -07:00
raise SyntaxError . new ( " Illegal nesting: nesting within a self-closing tag is illegal. " , 1 ) if @block_opened && self_closing
2008-04-18 11:43:29 -07:00
raise SyntaxError . new ( " Illegal nesting: content can't be both given on the same line as % #{ tag_name } and nested within it. " , 1 ) if @block_opened && ! value . empty?
2008-04-18 12:25:00 -07:00
raise SyntaxError . new ( " There's no Ruby code for #{ action } to evaluate. " ) if parse && value . empty?
2008-04-18 12:54:15 -07:00
raise SyntaxError . new ( " Self-closing tags can't have content. " ) if self_closing && ! value . empty?
2007-11-25 23:56:54 +00:00
2008-04-18 12:54:15 -07:00
self_closing || = ! ! ( ! @block_opened && value . empty? && @options [ :autoclose ] . include? ( tag_name ) )
2008-03-02 16:12:57 -08:00
2008-05-10 03:23:47 -07:00
dont_indent_next_line =
( nuke_outer_whitespace && ! @block_opened ) ||
( nuke_inner_whitespace && @block_opened )
2008-05-09 16:00:08 -07:00
# Check if we can render the tag directly to text and not process it in the buffer
2008-04-24 19:14:52 -07:00
if object_ref == " nil " && attributes_hash . nil? && ! preserve_script
2008-05-09 16:00:08 -07:00
tag_closed = ! @block_opened && ! self_closing && ! parse
2007-11-25 23:56:54 +00:00
2008-04-18 12:54:15 -07:00
open_tag = prerender_tag ( tag_name , self_closing , attributes )
2008-05-10 00:26:19 -07:00
if tag_closed
open_tag << " #{ value } </ #{ tag_name } > "
open_tag << " \n " unless nuke_outer_whitespace
else
2008-05-10 03:23:47 -07:00
open_tag << " \n " unless parse || nuke_inner_whitespace || ( self_closing && nuke_outer_whitespace )
2008-05-10 00:26:19 -07:00
end
2008-05-10 03:23:47 -07:00
push_merged_text ( open_tag , tag_closed || self_closing || nuke_inner_whitespace ? 0 : 1 ,
2008-05-10 00:26:19 -07:00
! nuke_outer_whitespace )
2007-11-25 23:56:54 +00:00
2008-05-10 03:23:47 -07:00
@dont_indent_next_line = dont_indent_next_line
2007-11-25 23:56:54 +00:00
return if tag_closed
else
flush_merged_text
content = value . empty? || parse ? 'nil' : value . dump
2007-12-19 09:41:33 +00:00
attributes_hash = ', ' + attributes_hash if attributes_hash
2008-05-10 00:26:19 -07:00
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 } ) "
2008-05-10 03:23:47 -07:00
@dont_tab_up_next_text = @dont_indent_next_line = dont_indent_next_line
2007-11-23 09:15:00 +00:00
end
2008-03-02 16:12:57 -08:00
2008-04-18 12:54:15 -07:00
return if self_closing
2007-11-23 09:15:00 +00:00
2007-11-25 23:56:54 +00:00
if value . empty?
2008-05-10 00:26:19 -07:00
push_and_tabulate ( [ :element , [ tag_name , nuke_outer_whitespace , nuke_inner_whitespace ] ] )
2008-05-10 03:23:47 -07:00
@output_tabs += 1 unless nuke_inner_whitespace
2007-11-25 23:56:54 +00:00
return
end
2008-04-07 23:09:17 -07:00
2007-11-25 23:56:54 +00:00
if parse
flush_merged_text
2008-05-10 03:23:47 -07:00
push_script ( value , preserve_script , true , preserve_tag , escape_html , nuke_inner_whitespace )
2008-05-10 00:26:19 -07:00
@dont_tab_up_next_text = true
concat_merged_text ( " </ #{ tag_name } > " + ( nuke_outer_whitespace ? " " : " \n " ) )
2007-11-23 09:15:00 +00:00
end
end
# Renders a line that creates an XHTML tag and has an implicit div because of
# <tt>.</tt> or <tt>#</tt>.
def render_div ( line )
render_tag ( '%div' + line )
end
# Renders an XHTML comment.
def render_comment ( line )
2008-04-28 20:59:43 -07:00
conditional , line = balance ( line , ?[ , ?] ) if line [ 0 ] == ?[
line . strip!
2007-11-23 09:15:00 +00:00
conditional << " > " if conditional
2008-04-07 23:09:17 -07:00
2008-04-28 20:59:43 -07:00
if @block_opened && ! line . empty?
2008-04-18 11:43:29 -07:00
raise SyntaxError . new ( 'Illegal nesting: nesting within a tag that already has content is illegal.' , 1 )
2007-11-23 09:15:00 +00:00
end
2007-11-26 00:36:03 +00:00
open = " <!-- #{ conditional } "
2008-04-07 23:09:17 -07:00
2007-11-26 00:36:03 +00:00
# Render it statically if possible
2008-04-28 20:59:43 -07:00
unless line . empty?
return push_text ( " #{ open } #{ line } #{ conditional ? " <![endif]--> " : " --> " } " )
2007-11-26 00:36:03 +00:00
end
push_text ( open , 1 )
@output_tabs += 1
push_and_tabulate ( [ :comment , ! conditional . nil? ] )
2008-04-28 20:59:43 -07:00
unless line . empty?
push_text ( line )
2007-11-26 00:36:03 +00:00
close
2007-11-23 09:15:00 +00:00
end
end
2008-04-07 23:09:17 -07:00
2007-11-23 09:15:00 +00:00
# Renders an XHTML doctype or XML shebang.
def render_doctype ( line )
2008-04-18 11:43:29 -07:00
raise SyntaxError . new ( " Illegal nesting: nesting within a header command is illegal. " , 1 ) if @block_opened
2008-02-27 15:16:21 +01:00
doctype = text_for_doctype ( line )
push_text doctype if doctype
2007-11-26 00:36:03 +00:00
end
def text_for_doctype ( text )
text = text [ 3 .. - 1 ] . lstrip . downcase
2008-02-27 15:16:21 +01:00
if text . index ( " xml " ) == 0
return nil if html?
2007-11-23 09:15:00 +00:00
wrapper = @options [ :attr_wrapper ]
2007-11-26 00:36:03 +00:00
return " <?xml version= #{ wrapper } 1.0 #{ wrapper } encoding= #{ wrapper } #{ text . split ( ' ' ) [ 1 ] || " utf-8 " } #{ wrapper } ?> "
end
2008-02-27 15:16:21 +01:00
if html5?
'<!DOCTYPE html>'
2008-02-24 17:10:30 -05:00
else
2008-02-27 15:16:21 +01:00
version , type = text . scan ( DOCTYPE_REGEX ) [ 0 ]
2008-04-07 23:09:17 -07:00
2008-02-27 15:16:21 +01:00
if xhtml?
if version == " 1.1 "
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
else
case type
when " strict " ; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
when " frameset " ; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
else '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
end
end
elsif html4?
case type
when " strict " ; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
when " frameset " ; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">'
else '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
end
2008-02-24 17:10:30 -05:00
end
2007-11-23 09:15:00 +00:00
end
end
2008-04-07 23:09:17 -07:00
2007-11-23 09:15:00 +00:00
# Starts a filtered block.
2007-11-25 20:20:44 +00:00
def start_filtered ( name )
2008-04-19 10:07:40 -07:00
raise Error . new ( " Invalid filter name \" : #{ name } \" . " ) unless name =~ / ^ \ w+$ /
2007-11-25 20:20:44 +00:00
unless filter = options [ :filters ] [ name ]
if filter == 'redcloth' || filter == 'markdown' || filter == 'textile'
2008-04-19 10:07:40 -07:00
raise Error . new ( " You must have the RedCloth gem installed to use \" #{ name } \" filter " )
2007-11-25 20:20:44 +00:00
end
2008-04-19 10:07:40 -07:00
raise Error . new ( " Filter \" #{ name } \" is not defined. " )
2007-11-23 09:15:00 +00:00
end
2007-11-25 20:20:44 +00:00
2007-11-23 09:15:00 +00:00
push_and_tabulate ( [ :filtered , filter ] )
@flat_spaces = @template_tabs * 2
@filter_buffer = String . new
2008-02-22 23:03:25 -08:00
@block_opened = false
end
def contains_interpolation? ( str )
str . include? ( '#{' )
2007-11-23 09:15:00 +00:00
end
def unescape_interpolation ( str )
2007-11-26 01:36:57 +00:00
scan = StringScanner . new ( str . dump )
str = ''
2007-11-23 09:15:00 +00:00
2008-02-23 01:09:23 -08:00
while scan . scan ( / (.*?)( \\ +) \ # \ { / )
escapes = ( scan [ 2 ] . size - 1 ) / 2
str << scan . matched [ 0 ... - 3 - escapes ]
if escapes % 2 == 1
str << '#{'
else
# Use eval to get rid of string escapes
str << '#{' + eval ( '"' + balance ( scan , ?{ , ?} , 1 ) [ 0 ] [ 0 ... - 1 ] + '"' ) + " } "
end
2007-11-26 01:36:57 +00:00
end
2007-11-23 09:15:00 +00:00
2007-11-26 01:36:57 +00:00
str + scan . rest
end
2007-11-23 09:15:00 +00:00
2008-02-10 19:45:36 -05:00
def balance ( scanner , start , finish , count = 0 )
2007-11-26 01:36:57 +00:00
str = ''
2008-02-10 19:45:36 -05:00
scanner = StringScanner . new ( scanner ) unless scanner . is_a? StringScanner
regexp = Regexp . new ( " (.*?)[ \\ #{ start . chr } \\ #{ finish . chr } ] " )
while scanner . scan ( regexp )
2007-11-26 01:36:57 +00:00
str << scanner . matched
2008-02-10 19:45:36 -05:00
count += 1 if scanner . matched [ - 1 ] == start
count -= 1 if scanner . matched [ - 1 ] == finish
return [ str . strip , scanner . rest ] if count == 0
2007-11-26 01:36:57 +00:00
end
2007-11-23 09:15:00 +00:00
2007-11-26 01:36:57 +00:00
raise SyntaxError . new ( " Unbalanced brackets. " )
2007-11-23 09:15:00 +00:00
end
# Counts the tabulation of a line.
def count_soft_tabs ( line )
2008-01-02 18:57:39 +00:00
spaces = line . index ( / ([^ ]|$) / )
2007-11-23 09:15:00 +00:00
if line [ spaces ] == ?\t
return nil if line . strip . empty?
2008-04-18 12:13:35 -07:00
raise SyntaxError . new ( <<END.strip, 2)
A tab character was used for indentation . Haml must be indented using two spaces .
Are you sure you have soft tabs enabled in your editor?
END
2007-11-23 09:15:00 +00:00
end
[ spaces , spaces / 2 ]
end
2008-04-07 23:09:17 -07:00
2007-11-23 09:15:00 +00:00
# Pushes value onto <tt>@to_close_stack</tt> and increases
# <tt>@template_tabs</tt>.
def push_and_tabulate ( value )
@to_close_stack . push ( value )
@template_tabs += 1
end
2007-11-25 19:36:06 +00:00
def flat?
@flat_spaces != - 1
end
2007-11-26 03:26:16 +00:00
2008-04-28 21:28:01 -07:00
def newline
@newlines += 1
end
def newline_now
2007-11-26 03:26:16 +00:00
@precompiled << " \n "
2008-04-28 21:28:01 -07:00
@newlines -= 1
end
def resolve_newlines
2008-05-02 00:48:39 -07:00
return unless @newlines > 0
2008-04-28 21:28:01 -07:00
@precompiled << " \n " * @newlines
@newlines = 0
2007-11-26 03:26:16 +00:00
end
2008-05-10 03:23:47 -07:00
# Get rid of and whitespace at the end of the buffer
# or the merged text
def rstrip_buffer!
unless @merged_text . empty?
@merged_text . rstrip!
else
push_silent ( " _erbout.rstrip! " , false )
@dont_tab_up_next_text = true
end
end
2007-11-23 09:15:00 +00:00
end
end