module Haml::Precompiler
def self.build_attributes(is_html, attr_wrapper, attributes = {})
def self.build_attributes(is_html, attr_wrapper, attributes = {}) quote_escape = attr_wrapper == '"' ? """ : "'" other_quote_char = attr_wrapper == '"' ? "'" : '"' result = attributes.collect do |attr, value| next if value.nil? if value == true next " #{attr}" if is_html next " #{attr}=#{attr_wrapper}#{attr}#{attr_wrapper}" elsif value == false next end value = Haml::Helpers.preserve(Haml::Helpers.escape_once(value.to_s)) # We want to decide whether or not to escape quotes value.gsub!('"', '"') 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 end end " #{attr}=#{this_attr_wrapper}#{value}#{this_attr_wrapper}" end result.compact.sort.join end
def balance(scanner, start, finish, count = 0)
def balance(scanner, start, finish, count = 0) str = '' scanner = StringScanner.new(scanner) unless scanner.is_a? StringScanner regexp = Regexp.new("(.*?)[\\#{start.chr}\\#{finish.chr}]") while scanner.scan(regexp) str << scanner.matched count += 1 if scanner.matched[-1] == start count -= 1 if scanner.matched[-1] == finish return [str.strip, scanner.rest] if count == 0 end raise SyntaxError.new("Unbalanced brackets.") end
def close
def close tag, value = @to_close_stack.pop case tag 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 end end
def close_block
def close_block push_silent "end", true @template_tabs -= 1 end
def close_comment(has_conditional)
def close_comment(has_conditional) @output_tabs -= 1 @template_tabs -= 1 close_tag = has_conditional ? "<![endif]-->" : "-->" push_text(close_tag, -1) end
def close_filtered(filter)
def close_filtered(filter) @flat_spaces = -1 filter.internal_compile(self, @filter_buffer) @filter_buffer = nil @template_tabs -= 1 end
def close_haml_comment
def close_haml_comment @haml_comment = false @template_tabs -= 1 end
def close_loud(command)
def close_loud(command) push_silent 'end', true @precompiled << command @template_tabs -= 1 end
def close_tag(value)
Puts a line in @precompiled that will add the closing tag of
def close_tag(value) tag, nuke_outer_whitespace, nuke_inner_whitespace = value @output_tabs -= 1 unless nuke_inner_whitespace @template_tabs -= 1 rstrip_buffer! if nuke_inner_whitespace push_merged_text("</#{tag}>" + (nuke_outer_whitespace ? "" : "\n"), nuke_inner_whitespace ? 0 : -1, !nuke_inner_whitespace) @dont_indent_next_line = nuke_outer_whitespace end
def concat_merged_text(text)
def concat_merged_text(text) @merged_text << text end
def contains_interpolation?(str)
def contains_interpolation?(str) str.include?('#{') end
def count_soft_tabs(line)
def count_soft_tabs(line) spaces = line.index(/([^ ]|$)/) if line[spaces] == ?\t return 0, 0 if line.strip.empty? raise SyntaxError.new(<<END.strip, @next_line.index) b character was used for indentation. Haml must be indented using two spaces. you sure you have soft tabs enabled in your editor? end [spaces, spaces/2] end
def flat?
def flat? @flat_spaces != -1 end
def flush_merged_text
def flush_merged_text return if @merged_text.empty? @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 << ");" @merged_text = '' @dont_tab_up_next_text = false @tab_change = 0 end
def handle_multiline(line)
This returns whether or not the line should be
the beginning, continuation, or end of a multiline sequence.
Deals with all the logic of figuring out whether a given line is
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 # 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 process_indent(line) 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 end return false end
def is_multiline?(text)
def is_multiline?(text) text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s end
def locals_code(names)
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(';') + ';' end
def mid_block_keyword?(text)
Returns whether or not the text is a silent script text with one
def mid_block_keyword?(text) text.length > 2 && text[0] == SILENT_SCRIPT && MID_BLOCK_KEYWORDS.include?(text[1..-1].split[0]) end
def newline
def newline @newlines += 1 end
def newline_now
def newline_now @precompiled << "\n" @newlines -= 1 end
def parse_attributes(line)
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
def parse_class_and_id(list)
and # syntax, and returns a hash with them as attributes,
Iterates through the classes and ids supplied through .
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 when '#'; attributes['id'] = property end end attributes end
def parse_literal_value(text)
def parse_literal_value(text) return nil unless text 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
def parse_static_hash(text)
def parse_static_hash(text) return {} unless text attributes = {} text.split(',').each do |attrib| key, value, more = attrib.split('=>') # Make sure the key and value and only the key and value exist # Otherwise, it's too complicated or dynamic and we'll defer it to the actual Ruby parser key = parse_literal_value key value = parse_literal_value value return nil if more || key.nil? || value.nil? attributes[key] = value end attributes end
def parse_tag(line)
def parse_tag(line) raise SyntaxError.new("Invalid tag: \"#{line}\".") unless match = line.scan(/%([-:\w]+)([-\w\.\#]*)(.*)/)[0] tag_name, attributes, rest = match attributes_hash, rest = parse_attributes(rest) if rest[0] == ?{ if rest object_ref, rest = balance(rest, ?[, ?]) if rest[0] == ?[ attributes_hash, rest = parse_attributes(rest) if rest[0] == ?{ && attributes_hash.nil? nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0] nuke_whitespace ||= '' nuke_outer_whitespace = nuke_whitespace.include? '>' nuke_inner_whitespace = nuke_whitespace.include? '<' end value = value.to_s.strip [tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace, nuke_inner_whitespace, action, value] end
def precompile
def precompile old_line = Line.new @template.split(/\r\n|\r|\n/).each_with_index do |text, index| @next_line = line = Line.new(text.strip, text.lstrip.chomp, index) line.spaces, line.tabs = count_soft_tabs(text) if line.text.empty? && !flat? newline next end suppress_render = handle_multiline(old_line) unless flat? if old_line.text.nil? || suppress_render old_line = line resolve_newlines newline next end process_indent(old_line) unless old_line.text.empty? if flat? push_flat(old_line) old_line = line newline next end if old_line.spaces != old_line.tabs * 2 raise SyntaxError.new(<<END.strip, old_line.index) d_line.spaces} space#{old_line.spaces == 1 ? ' was' : 's were'} used for indentation. Haml must be indented using two spaces. end unless old_line.text.empty? || @haml_comment process_line(old_line.text, old_line.index, line.tabs > old_line.tabs && !line.text.empty?) end resolve_newlines if !flat? && line.tabs - old_line.tabs > 1 raise SyntaxError.new(<<END.strip, line.index) ne.spaces} spaces were used for indentation. Haml must be indented using two spaces. end old_line = line newline end # Close all the open tags close until @to_close_stack.empty? flush_merged_text end
def precompiled_with_ambles(local_names)
def precompiled_with_ambles(local_names) preamble = <<END.gsub("\n", ";") nd Haml::Helpers lout = @haml_buffer = Haml::Buffer.new(@haml_buffer, #{options_for_buffer.inspect}) out = _hamlout.buffer postamble = <<END.gsub("\n", ";") l_buffer = @haml_buffer.upper out preamble + locals_code(local_names) + @precompiled + postamble end
def prerender_tag(name, self_close, attributes)
def prerender_tag(name, self_close, attributes) attributes_string = Precompiler.build_attributes(html?, @options[:attr_wrapper], attributes) "<#{name}#{attributes_string}#{self_close && xhtml? ? ' /' : ''}>" end
def process_indent(line)
def process_indent(line) return unless line.tabs <= @template_tabs && @template_tabs > 0 to_close = @template_tabs - line.tabs to_close.times { |i| close unless to_close - 1 - i == 0 && mid_block_keyword?(line.text) } end
def process_line(text, index, block_opened)
This method doesn't return anything; it simply processes the line and
Processes a single line of Haml.
def process_line(text, index, block_opened) @block_opened = block_opened @index = index + 1 case text[0] when DIV_CLASS, DIV_ID; render_div(text) when ELEMENT; render_tag(text) when COMMENT; render_comment(text[1..-1].strip) when SANITIZE 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 push_plain text when SCRIPT return push_script(unescape_interpolation(text[2..-1].strip), false) if text[1] == SCRIPT return push_script(text[1..-1], false, false, false, true) if options[:escape_html] push_script(text[1..-1], false) when FLAT_SCRIPT; push_flat_script(text[1..-1]) when SILENT_SCRIPT return start_haml_comment if text[1] == SILENT_COMMENT raise SyntaxError.new(<<END.rstrip, index) if text[1..-1].strip == "end" don't need to use "- end" in Haml. Use indentation instead: foo? trong Foo! se t foo. push_silent(text[1..-1], true) newline_now if (@block_opened && !mid_block_keyword?(text)) || text[1..-1].split(' ', 2)[0] == "case" push_and_tabulate([:script]) end when FILTER; start_filtered(text[1..-1].downcase) when DOCTYPE return render_doctype(text) if text[0...3] == '!!!' return push_script(unescape_interpolation(text[3..-1].strip), false) if text[1..2] == "==" return push_script(text[2..-1].strip, false) if text[1] == SCRIPT push_plain text when ESCAPE; push_plain text[1..-1] else push_plain text end end
def push_and_tabulate(value)
Pushes value onto @to_close_stack and increases
def push_and_tabulate(value) @to_close_stack.push(value) @template_tabs += 1 end
def push_flat(line)
def push_flat(line) tabulation = line.spaces - @flat_spaces tabulation = tabulation > -1 ? tabulation : 0 @filter_buffer << "#{' ' * tabulation}#{line.unstripped}\n" end
def push_flat_script(text)
Causes text to be evaluated, and Haml::Helpers#find_and_flatten
def push_flat_script(text) flush_merged_text raise SyntaxError.new("There's no Ruby code for ~ to evaluate.") if text.empty? push_script(text, true) end
def push_merged_text(text, tab_change = 0, indent = true)
Adds text to @buffer with appropriate tabulation
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 @tab_change += tab_change end
def push_plain(text)
Renders a block of text as plain text.
def push_plain(text) if @block_opened raise SyntaxError.new("Illegal nesting: nesting within plain text is illegal.", @next_line.index) end push_text text end
def push_script(text, preserve_script, in_tag = false, preserve_tag = false,
If preserve_script is true, Haml::Helpers#find_and_flatten is run on
the scope object and the result to be added to @buffer.
Causes text to be evaluated in the context of
def push_script(text, preserve_script, in_tag = false, preserve_tag = false, escape_html = false, nuke_inner_whitespace = false) # Prerender tabulation unless we're in a tag push_merged_text '' unless in_tag flush_merged_text return if options[:suppress_eval] raise SyntaxError.new("There's no Ruby code for = to evaluate.") if text.empty? push_silent "haml_temp = #{text}" newline_now args = [preserve_script, in_tag, preserve_tag, escape_html, nuke_inner_whitespace].map { |a| a.inspect }.join(', ') out = "haml_temp = _hamlout.push_script(haml_temp, #{args});" if @block_opened push_and_tabulate([:loud, out]) else @precompiled << out end end
def push_silent(text, can_suppress = false)
Evaluates text in the context of the scope object, but
def push_silent(text, can_suppress = false) flush_merged_text return if can_suppress && options[:suppress_eval] @precompiled << "#{text};" end
def push_text(text, tab_change = 0)
def push_text(text, tab_change = 0) push_merged_text("#{text}\n", tab_change) end
def render_comment(line)
def render_comment(line) conditional, line = balance(line, ?[, ?]) if line[0] == ?[ line.strip! conditional << ">" if conditional if @block_opened && !line.empty? raise SyntaxError.new('Illegal nesting: nesting within a tag that already has content is illegal.', @next_line.index) end open = "<!--#{conditional} " # Render it statically if possible unless line.empty? return push_text("#{open}#{line} #{conditional ? "<![endif]-->" : "-->"}") end push_text(open, 1) @output_tabs += 1 push_and_tabulate([:comment, !conditional.nil?]) unless line.empty? push_text(line) close end end
def render_div(line)
Renders a line that creates an XHTML tag and has an implicit div because of
def render_div(line) render_tag('%div' + line) end
def render_doctype(line)
def render_doctype(line) raise SyntaxError.new("Illegal nesting: nesting within a header command is illegal.", @next_line.index) if @block_opened doctype = text_for_doctype(line) push_text doctype if doctype end
def render_tag(line)
Parses a line that will render as an XHTML tag, and adds the code that will
def render_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)/ # Get rid of whitespace outside of the tag if we need to rstrip_buffer! if nuke_outer_whitespace preserve_tag = options[:preserve].include?(tag_name) nuke_inner_whitespace ||= preserve_tag preserve_tag &&= !options[:ugly] case action when '/'; self_closing = xhtml? when '~'; parse = preserve_script = true when '=' parse = true value = unescape_interpolation(value[1..-1].strip) if value[0] == ?= when '&', '!' if value[0] == ?= parse = true value = (value[1] == ?= ? unescape_interpolation(value[2..-1].strip) : value[1..-1].strip) end end if parse && @options[:suppress_eval] parse = false value = '' end escape_html = (action == '&' || (action != '!' && @options[:escape_html])) object_ref = "nil" if object_ref.nil? || @options[:suppress_eval] static_attributes = parse_static_hash(attributes_hash) # Try pre-compiling a static attributes hash attributes_hash = nil if static_attributes || @options[:suppress_eval] attributes = parse_class_and_id(attributes) Buffer.merge_attrs(attributes, static_attributes) if static_attributes raise SyntaxError.new("Illegal nesting: nesting within a self-closing tag is illegal.", @next_line.index) if @block_opened && self_closing raise SyntaxError.new("Illegal nesting: content can't be both given on the same line as %#{tag_name} and nested within it.", @next_line.index) if @block_opened && !value.empty? raise SyntaxError.new("There's no Ruby code for #{action} to evaluate.") if parse && value.empty? raise SyntaxError.new("Self-closing tags can't have content.") if self_closing && !value.empty? self_closing ||= !!( !@block_opened && value.empty? && @options[:autoclose].include?(tag_name) ) dont_indent_next_line = (nuke_outer_whitespace && !@block_opened) || (nuke_inner_whitespace && @block_opened) # Check if we can render the tag directly to text and not process it in the buffer if object_ref == "nil" && attributes_hash.nil? && !preserve_script tag_closed = !@block_opened && !self_closing && !parse open_tag = prerender_tag(tag_name, self_closing, attributes) if tag_closed open_tag << "#{value}</#{tag_name}>" open_tag << "\n" unless nuke_outer_whitespace else open_tag << "\n" unless parse || nuke_inner_whitespace || (self_closing && nuke_outer_whitespace) end push_merged_text(open_tag, tag_closed || self_closing || nuke_inner_whitespace ? 0 : 1, !nuke_outer_whitespace) @dont_indent_next_line = dont_indent_next_line return if tag_closed else flush_merged_text content = value.empty? || parse ? 'nil' : value.dump attributes_hash = ', ' + attributes_hash if 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 = dont_indent_next_line end return if self_closing if value.empty? push_and_tabulate([:element, [tag_name, nuke_outer_whitespace, nuke_inner_whitespace]]) @output_tabs += 1 unless nuke_inner_whitespace return end if parse flush_merged_text push_script(value, preserve_script, true, preserve_tag, escape_html, nuke_inner_whitespace) @dont_tab_up_next_text = true concat_merged_text("</#{tag_name}>" + (nuke_outer_whitespace ? "" : "\n")) end end
def resolve_newlines
def resolve_newlines return unless @newlines > 0 @precompiled << "\n" * @newlines @newlines = 0 end
def rstrip_buffer!
Get rid of and whitespace at the end of the buffer
def rstrip_buffer! unless @merged_text.empty? @merged_text.rstrip! else push_silent("_erbout.rstrip!", false) @dont_tab_up_next_text = true end end
def start_filtered(name)
def start_filtered(name) raise Error.new("Invalid filter name \":#{name}\".") unless name =~ /^\w+$/ unless filter = Filters.defined[name] if filter == 'redcloth' || filter == 'markdown' || filter == 'textile' raise Error.new("You must have the RedCloth gem installed to use \"#{name}\" filter") end raise Error.new("Filter \"#{name}\" is not defined.") end push_and_tabulate([:filtered, filter]) @flat_spaces = @template_tabs * 2 @filter_buffer = String.new @block_opened = false end
def start_haml_comment
def start_haml_comment return unless @block_opened @haml_comment = true push_and_tabulate([:haml_comment]) end
def text_for_doctype(text)
def text_for_doctype(text) text = text[3..-1].lstrip.downcase if text.index("xml") == 0 return nil if html? wrapper = @options[:attr_wrapper] return "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{text.split(' ')[1] || "utf-8"}#{wrapper} ?>" end if html5? '<!DOCTYPE html>' else version, type = text.scan(DOCTYPE_REGEX)[0] 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 end end end
def unescape_interpolation(str)
def unescape_interpolation(str) scan = StringScanner.new(str.dump) str = '' 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 end str + scan.rest end