require 'markaby/tags'
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 => 0,
:output_helpers => true,
:output_xml_instruction => true,
:output_meta_tag => true,
:auto_validation => true,
:tagset => Markaby::XHTMLTransitional,
:root_attributes => {
:xmlns => 'http://www.w3.org/1999/xhtml', :'xml:lang' => 'en', :lang => 'en'
}
}
def self.set(option, value)
@@default[option] = value
end
def self.ignored_helpers
@@ignored_helpers ||= []
end
def self.ignore_helpers(*helpers)
ignored_helpers.concat helpers
end
attr_accessor :output_helpers, :tagset
# 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)
@streams = [[]]
@assigns = assigns.dup
@helpers = helpers
@elements = {}
@@default.each do |k, v|
instance_variable_set("@#{k}", @assigns.delete(k) || v)
end
@assigns.each do |k, v|
instance_variable_set("@#{k}", v)
end
@builder = XmlMarkup.new(:indent => @indent, :target => @streams.last)
text(capture(&block)) if block
end
# Returns a string containing the HTML stream. Internally, the stream is stored as an Array.
def to_s
@streams.last.to_s
end
# Write a +string+ to the HTML stream without escaping it.
def text(string)
@builder << string.to_s
nil
end
alias_method :<<, :text
alias_method :concat, :text
# 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
\nCAPTURE ME
\n"
#
def capture(&block)
@streams.push(@builder.target = [])
@builder.level += 1
str = instance_eval(&block)
str = @streams.last.join if @streams.last.any?
@streams.pop
@builder.level -= 1
@builder.target = @streams.last
str
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)
ele_id = nil
if @auto_validation and @tagset
if !@tagset.tagset.has_key?(tag)
raise InvalidXhtmlError, "no element `#{tag}' for #{tagset.doctype}"
elsif args.last.respond_to?(:to_hash)
attrs = args.last.to_hash
if @tagset.forms.include?(tag) and attrs[:id]
attrs[:name] ||= attrs[:id]
end
attrs.each do |k, v|
atname = k.to_s.downcase.intern
unless k =~ /:/ or @tagset.tagset[tag].include? atname
raise InvalidXhtmlError, "no attribute `#{k}' on #{tag} elements"
end
if atname == :id
ele_id = v.to_s
if @elements.has_key? ele_id
raise InvalidXhtmlError, "id `#{ele_id}' already used (id's must be unique)."
end
end
end
end
end
if block
str = capture(&block)
block = proc { text(str) }
end
f = fragment { @builder.method_missing(tag, *args, &block) }
@elements[ele_id] = f if ele_id
f
end
# This method is used to intercept calls to helper methods and instance
# variables. Here is the order of interception:
#
# * If +sym+ is a helper method, the helper method is called
# and output to the stream.
# * If +sym+ is a Builder::XmlMarkup method, it is passed on to the builder object.
# * If +sym+ is also the name of an instance variable, the
# value of the instance variable is returned.
# * If +sym+ has come this far and no +tagset+ is found, +sym+ and its arguments are passed to tag!
# * If a tagset is found, though, +NoMethodError+ is raised.
#
# method_missing used to be the lynchpin in Markaby, but it's no longer used to handle
# HTML tags. See html_tag for that.
def method_missing(sym, *args, &block)
if @helpers.respond_to?(sym, true) && !self.class.ignored_helpers.include?(sym)
r = @helpers.send(sym, *args, &block)
if @output_helpers and r.respond_to? :to_str
@helpers.output_buffer = '' if @helpers.respond_to?(:output_buffer=)
fragment { @builder << r }
else
r
end
elsif @assigns.has_key?(sym)
@assigns[sym]
elsif @assigns.has_key?(stringy_key = sym.to_s)
# Rails' ActionView assigns hash has string keys for
# instance variables that are defined in the controller.
@assigns[stringy_key]
elsif instance_variables.include?(ivar = "@#{sym}")
instance_variable_get(ivar)
elsif !@helpers.nil? && @helpers.instance_variables.include?(ivar)
@helpers.instance_variable_get(ivar)
elsif ::Builder::XmlMarkup.instance_methods.include?(sym.to_s)
@builder.__send__(sym, *args, &block)
elsif @tagset.nil?
tag!(sym, *args, &block)
else
raise NoMethodError, "no such method `#{sym}'"
end
end
# Every HTML tag method goes through an html_tag call. So, calling div is equivalent
# to calling html_tag(:div). All HTML tags in Markaby's list are given generated wrappers
# for this method.
#
# If the @auto_validation setting is on, this method will check for many common mistakes which
# could lead to invalid XHTML.
def html_tag(sym, *args, &block)
if @auto_validation and @tagset.self_closing.include?(sym) and block
raise InvalidXhtmlError, "the `#{sym}' element is self-closing, please remove the block"
elsif args.empty? and block.nil?
CssProxy.new(self, @streams.last, sym)
else
tag!(sym, *args, &block)
end
end
XHTMLTransitional.tags.each do |k|
class_eval %{
def #{k}(*args, &block)
html_tag(#{k.inspect}, *args, &block)
end
}
end
remove_method :head
# 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",
# :lang => "en".
def xhtml_transitional(attrs = {}, &block)
self.tagset = Markaby::XHTMLTransitional
xhtml_html(attrs, &block)
end
# Builds an html tag with XHTML 1.0 Strict doctype instead.
def xhtml_strict(attrs = {}, &block)
self.tagset = Markaby::XHTMLStrict
xhtml_html(attrs, &block)
end
# Builds an html tag with XHTML 1.0 Frameset doctype instead.
def xhtml_frameset(attrs = {}, &block)
self.tagset = Markaby::XHTMLFrameset
xhtml_html(attrs, &block)
end
private
def xhtml_html(attrs = {}, &block)
instruct! if @output_xml_instruction
declare!(:DOCTYPE, :html, :PUBLIC, *tagset.doctype)
tag!(:html, @root_attributes.merge(attrs), &block)
end
def fragment
stream = @streams.last
start = stream.length
yield
length = stream.length - start
Fragment.new(stream, start, length)
end
end
# Every tag method in Markaby returns a Fragment. If any method gets called on the Fragment,
# the tag is removed from the Markaby stream and given back as a string. Usually the fragment
# is never used, though, and the stream stays intact.
#
# For a more practical explanation, check out the README.
class Fragment < ::Builder::BlankSlate
def initialize(*args)
@stream, @start, @length = args
end
def method_missing(*args, &block)
# We can't do @stream.slice!(@start, @length),
# as it would invalidate the @starts and @lengths of other Fragment instances.
@str = @stream[@start, @length].to_s
@stream[@start, @length] = [nil] * @length
def self.method_missing(*args, &block)
@str.send(*args, &block)
end
@str.send(*args, &block)
end
end
class XmlMarkup < ::Builder::XmlMarkup
attr_accessor :target, :level
end
end