lib/markaby/builder.rb in markaby-0.2 vs lib/markaby/builder.rb in markaby-0.3

- old
+ new

@@ -1,112 +1,189 @@ module Markaby + # The Markaby::Builder class is the central gear in the system. When using + # from Ruby code, this is the only class you need to instantiate directly. + # + # mab = Markaby::Builder.new + # mab.html do + # head { title "Boats.com" } + # body do + # h1 "Boats.com has great deals" + # ul do + # li "$49 for a canoe" + # li "$39 for a raft" + # li "$29 for a huge boot that floats and can fit 5 people" + # end + # end + # end + # puts mab.to_s + # class Builder attr_accessor :output_helpers - def initialize(assigns, helpers, &block) - @assigns = assigns - @helpers = helpers.dup + # Create a Markaby builder object. Pass in a hash of variable assignments to + # +assigns+ which will be available as instance variables inside tag construction + # blocks. If an object is passed in to +helpers+, its methods will be available + # from those same blocks. + # + # Pass in a +block+ to new and the block will be evaluated. + # + # mab = Markaby::Builder.new { + # html do + # body do + # h1 "Matching Mole" + # end + # end + # } + # + def initialize(assigns = {}, helpers = nil, &block) @stream = [] + @assigns = assigns @builder = ::Builder::XmlMarkup.new(:indent => 2, :target => @stream) @output_helpers = true - for iv in helpers.instance_variables - instance_variable_set(iv, helpers.instance_variable_get(iv)) + if helpers.nil? + @helpers = nil + else + @helpers = helpers.dup + for iv in helpers.instance_variables + instance_variable_set(iv, helpers.instance_variable_get(iv)) + end end - for iv, val in assigns - instance_variable_set("@#{iv}", val) - @helpers.instance_variable_set("@#{iv}", val) + + unless assigns.nil? || assigns.empty? + for iv, val in assigns + instance_variable_set("@#{iv}", val) + unless @helpers.nil? + @helpers.instance_variable_set("@#{iv}", val) + end + end end if block r = instance_eval &block text(r) if to_s.empty? end end + # Returns a string containing the HTML stream. Internally, the stream is stored as an Array. def to_s - @builder.target! + @builder.target!.join end + # Write a +string+ to the HTML stream without escaping it. def text(string) @builder << "#{string}" nil end alias_method :<<, :text + # Captures the HTML code built inside the +block+. This is done by creating a new + # builder object, running the block and passing back its stream as a string. + # + # >> Markaby::Builder.new.capture { h1 "TEST"; h2 "CAPTURE ME" } + # => "<h1>TITLE</h1>\n<h2>CAPTURE ME</h2>\n" + # def capture(&block) assigns = instance_variables.inject({}) do |hsh, iv| unless ['@stream', '@builder', '@assigns', '@helpers'].include?(iv) hsh[iv[1..-1]] = instance_variable_get(iv) end hsh end self.class.new(assigns, @helpers, &block).to_s end + # Content_for will store the given block in an instance variable for later use + # in another template or in the layout. + # + # The name of the instance variable is content_for_<name> to stay consistent + # with @content_for_layout which is used by ActionView's layouts. + # + # Example: + # + # content_for("header") do + # h1 "Half Shark and Half Lion" + # end + # + # If used several times, the variable will contain all the parts concatenated. def content_for(name, &block) eval "@content_for_#{name} = (@content_for_#{name} || '') + capture(&block)" end + # Create a tag named +tag+. Other than the first argument which is the tag name, + # the arguments are the same as the tags implemented via method_missing. def tag!(tag, *args, &block) if block str = capture &block block = proc { text(str) } end @builder.method_missing(tag, *args, &block) end - def method_missing(tag, *args, &block) + # Create XML markup based on the name of the method +sym+. This method is never + # invoked directly, but is called for each markup method in the markup block. + # + # This method is also used to intercept calls to helper methods and instance + # variables. Here is the order of interception: + # + # * If +sym+ is a recognized HTML tag, the tag is output + # or a CssProxy is returned if no arguments are given. + # * If +sym+ appears to be a self-closing tag, its block + # is ignored, thus outputting a valid self-closing tag. + # * If +sym+ is also the name of an instance variable, the + # value of the instance variable is returned. + # * If +sym+ is a helper method, the helper method is called + # and output to the stream. + # * Otherwise, +sym+ and its arguments are passed to tag! + def method_missing(sym, *args, &block) args.each do |arg| @stream.delete_if { |x| x.object_id == arg.object_id } end - - if (TAGS + BIG_TAGS).include?(tag) + if TAGS.include?(sym) if args.empty? and block.nil? return CssProxy.new do |args, block| - tag!(tag, *args, &block) + tag!(sym, *args, &block) end end - end - - if TAGS.include?(tag) - tag!(tag, *args, &block) - elsif BIG_TAGS.include?(tag) - tag!(tag, *args, &block) - elsif SELF_CLOSING_TAGS.include?(tag) - tag!(tag, *args) - elsif instance_variable_get("@#{tag}") - instance_variable_get("@#{tag}") - elsif @helpers.respond_to?(tag) - r = @helpers.send(tag, *args, &block) + tag!(sym, *args, &block) + elsif SELF_CLOSING_TAGS.include?(sym) + tag!(sym, *args) + elsif instance_variable_get("@#{sym}") + instance_variable_get("@#{sym}") + elsif @helpers.respond_to?(sym) + r = @helpers.send(sym, *args, &block) @builder << r if @output_helpers r else - tag!(tag, *args, &block) + tag!(sym, *args, &block) end end def p(*args, &block) method_missing(:p, *args, &block) end @@default_image_tag_options ||= { :border => '0', :alt => '' } + # Builds a image tag. Assumes <tt>:border => '0', :alt => ''</tt>. def img(opts = {}) - opts[:border] ||= '0' - opts[:alt] ||= '' tag!(:img, @@default_image_tag_options.merge(opts)) end + # Builds a head tag. Adds a <tt>meta</tt> tag inside with Content-Type + # set to <tt>text/html; charset=utf-8</tt>. def head(*args, &block) tag!(:head, *args) do tag!(:meta, 'http-equiv' => 'Content-Type', 'content' => 'text/html; charset=utf-8') instance_eval &block end end + # Builds an html tag. An XML 1.0 instruction and an XHTML 1.0 Transitional doctype + # are prepended. Also assumes <tt>:xmlns => "http://www.w3.org/1999/xhtml", + # "xml:lang" => "en", :lang => "en"</tt>. def html(*args, &block) if args.empty? args = ["-//W3C//DTD XHTML 1.0 Transitional//EN", "DTD/xhtml1-transitional.dtd"] end @builder.instruct! @@ -114,9 +191,10 @@ tag!(:html, :xmlns => "http://www.w3.org/1999/xhtml", "xml:lang" => "en", :lang => "en", &block) end alias_method :xhtml_transitional, :html + # Builds an html tag with XHTML 1.0 Strict doctype instead. def xhtml_strict(&block) html("-//W3C//DTD XHTML 1.0 Strict//EN", "DTD/xhtml1-strict.dtd", &block) end end end