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 @@default = { :indent => 2, :output_helpers => true, :output_xml_instruction => true, :output_meta_tag => true, :image_tag_options => { :border => '0', :alt => '' } } def self.set(option, value) @@default[option] = value end XHTMLTransitional = ["-//W3C//DTD XHTML 1.0 Transitional//EN", "DTD/xhtml1-transitional.dtd"] XHTMLStrict = ["-//W3C//DTD XHTML 1.0 Strict//EN", "DTD/xhtml1-strict.dtd"] attr_accessor :output_helpers # 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 @margin = -1 @indent = @@default[:indent] @output_helpers = @@default[:output_helpers] @output_meta_tag = @@default[:output_meta_tag] @output_xml_instruction = @@default[:output_xml_instruction] 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 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 @margin += 1 @builder = ::Builder::XmlMarkup.new(:indent => @indent, :margin => @margin, :target => @stream) 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 @stream.join end # Write a +string+ to the HTML stream without escaping it. def text(string) @builder << "#{string}" nil end alias_method :<<, :text alias_method :concat, :text # Emulate ERB to satisfy helpers like form_for. def _erbout; self end # Captures the HTML code built inside the +block+. This is done by creating a new # stream for the builder object, running the block and passing back its stream as a string. # # >> Markaby::Builder.new.capture { h1 "TEST"; h2 "CAPTURE ME" } # => "

TITLE

\n

CAPTURE ME

\n" # def capture(&block) old_stream = @stream.dup @stream.replace [] str = instance_eval(&block).to_s str = @stream.join unless @stream.empty? @stream.replace old_stream str 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_ 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 # 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) if TAGS.include?(sym) or (FORM_TAGS.include?(sym) and args.empty?) if args.empty? and block.nil? return CssProxy.new do |args, block| if FORM_TAGS.include?(sym) and args.last.respond_to?(:to_hash) and args.last[:id] args.last[:name] ||= args.last[:id] end tag!(sym, *args, &block) end end if args.first.respond_to? :to_hash block ||= proc{} end tag!(sym, *args, &block) elsif SELF_CLOSING_TAGS.include?(sym) tag!(sym, *args) elsif @helpers.respond_to?(sym) r = @helpers.send(sym, *args, &block) @builder << r if @output_helpers r elsif instance_variable_get("@#{sym}") instance_variable_get("@#{sym}") else tag!(sym, *args, &block) end end undef_method :p undef_method :select # Builds a image tag. Assumes :border => '0', :alt => ''. def img(opts = {}) tag!(:img, @@default[:image_tag_options].merge(opts)) end # Builds a head tag. Adds a meta tag inside with Content-Type # set to text/html; charset=utf-8. def head(*args, &block) tag!(:head, *args) do tag!(:meta, "http-equiv" => "Content-Type", "content" => "text/html; charset=utf-8") if @output_meta_tag 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 :xmlns => "http://www.w3.org/1999/xhtml", # "xml:lang" => "en", :lang => "en". def html(*doctype, &block) doctype = XHTMLTransitional if doctype.empty? @builder.instruct! if @output_xml_instruction @builder.declare!(:DOCTYPE, :html, :PUBLIC, *doctype) 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 XHTMLStrict, &block end end end