require 'temple' require 'fast_haml/ast' require 'fast_haml/filter_compilers' require 'fast_haml/static_hash_parser' require 'fast_haml/text_compiler' module FastHaml class Compiler < Temple::Parser DEFAULT_AUTO_CLOSE_TAGS = %w[ area base basefont br col command embed frame hr img input isindex keygen link menuitem meta param source track wbr ] DEFAULT_PRESERVE_TAGS = %w[pre textarea code] define_options( autoclose: DEFAULT_AUTO_CLOSE_TAGS, format: :html, preserve: DEFAULT_PRESERVE_TAGS, ) def initialize(*) super @text_compiler = TextCompiler.new end def call(ast) compile(ast) end def self.find_and_preserve(input) # Taken from the original haml code re = /<(#{options[:preserve].map(&Regexp.method(:escape)).join('|')})([^>]*)>(.*?)(<\/\1>)/im input.to_s.gsub(re) do |s| s =~ re # Can't rely on $1, etc. existing since Rails' SafeBuffer#gsub is incompatible "<#{$1}#{$2}>#{preserve($3)}" end end def self.preserve(input) # Taken from the original haml code input.to_s.chomp("\n").gsub(/\n/, ' ').gsub(/\r/, '') end private def compile(ast) temple = case ast when Ast::Root compile_root(ast) when Ast::Doctype compile_doctype(ast) when Ast::HtmlComment compile_html_comment(ast) when Ast::HamlComment [:multi] when Ast::Element compile_element(ast) when Ast::Script compile_script(ast) when Ast::SilentScript compile_silent_script(ast) when Ast::Text compile_text(ast) when Ast::Filter compile_filter(ast) else raise "InternalError: Unknown AST node #{ast.class}: #{ast.inspect}" end if ast.trailing_empty_lines > 0 [:multi, temple].concat([[:newline]] * ast.trailing_empty_lines) else temple end end def compile_root(ast) [:multi].tap do |temple| compile_children(ast, temple) end end def compile_children(ast, temple) was_newline = false ast.children.each do |c| if c.is_a?(Ast::Element) && c.nuke_outer_whitespace && was_newline # pop newline x = temple.pop if x != [:newline] raise "InternalError: Unexpected pop (expected [:newline]): #{x}" end x = temple.pop if x != [:static, "\n"] raise "InternalError: Unexpected pop (expected [:static, newline]): #{x}" end unless suppress_code_newline?(c.oneline_child) temple << [:newline] end end temple << compile(c) if was_newline = need_newline?(ast, c) temple << [:static, "\n"] end unless suppress_code_newline?(c) temple << [:newline] end end end def need_newline?(parent, child) if parent.is_a?(Ast::Element) && nuke_inner_whitespace?(parent) && parent.children.last.equal?(child) return false end case child when Ast::Script child.children.empty? when Ast::SilentScript, Ast::HamlComment false when Ast::Element !child.nuke_outer_whitespace else true end end def suppress_code_newline?(ast) ast.is_a?(Ast::Script) || ast.is_a?(Ast::SilentScript) || (ast.is_a?(Ast::Element) && suppress_code_newline?(ast.oneline_child)) end def compile_text(ast) @text_compiler.compile(ast.text, escape_html: ast.escape_html) end # html5 and html4 is deprecated in temple. DEFAULT_DOCTYPE = { html: 'html', html5: 'html', html4: 'transitional', xhtml: 'transitional', }.freeze def compile_doctype(ast) doctype = ast.doctype.downcase if doctype.empty? doctype = DEFAULT_DOCTYPE[options[:format]] end [:haml, :doctype, doctype] end def compile_html_comment(ast) if ast.children.empty? if ast.conditional.empty? [:html, :comment, [:static, " #{ast.comment} "]] else [:html, :comment, [:static, "[#{ast.conditional}]> #{ast.comment} \n"] end compile_children(ast, temple) unless ast.conditional.empty? temple << [:static, "