require "markaby/tagset"
require "markaby/builder_tags"
module Markaby
RUBY_VERSION_ID = RUBY_VERSION.split(".").join.to_i
# 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
include Markaby::BuilderTags
GENERIC_OPTIONS = {
indent: 0,
auto_validation: true
}
HTML5_OPTIONS = HTML5.default_options.dup
DEFAULT_OPTIONS = GENERIC_OPTIONS.merge(HTML5_OPTIONS)
@@options = DEFAULT_OPTIONS.dup
def self.restore_defaults!
@@options = DEFAULT_OPTIONS.dup
end
def self.set(option, value)
@@options[option] = value
end
def self.get(option)
@@options[option]
end
attr_reader :tagset
def tagset=(tagset)
@tagset = tagset
tagset.default_options.each do |k, v|
instance_variable_set("@#{k}".to_sym, v)
end
end
# 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 +helper+, 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 = {}, helper = nil, &block)
@streams = [Stream.new]
@assigns = assigns.dup
@_helper = helper
@used_ids = {}
@@options.each do |k, v|
instance_variable_set("@#{k}", @assigns.delete(k) || v)
end
@assigns.each do |k, v|
instance_variable_set("@#{k}", v)
end
helper&.instance_variables&.each do |iv|
instance_variable_set(iv, helper.instance_variable_get(iv))
end
@builder = XmlMarkup.new(indent: @indent, target: @streams.last)
text(capture(&block)) if block
end
def helper=(helper)
@_helper = helper
end
def locals=(locals)
locals.each do |key, value|
define_singleton_method(key) { value }
end
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" }
# => "
TEST
CAPTURE ME
"
#
def capture(&block)
@streams.push(@builder.target = Stream.new)
@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)
attributes = {}
if @auto_validation && @tagset
attributes = @tagset.validate_and_transform_attributes!(tag, *args)
tag = @tagset.validate_and_transform_tag_name! tag
end
element_id = attributes[:id].to_s
raise InvalidXhtmlError, "id `#{element_id}' already used (id's must be unique)." if @used_ids.has_key?(element_id)
if block
str = capture(&block)
block = proc { text(str) }
end
f = fragment { @builder.tag!(tag, *args, &block) }
@used_ids[element_id] = f unless element_id.empty?
f
end
private
# 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, the tagset is tole to handle +sym+
#
# 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)
case response_for(sym)
when :helper then @_helper.send(sym, *args, &block)
when :assigns then @assigns[sym]
when :stringy_assigns then @assigns[sym.to_s]
when :ivar then instance_variable_get(ivar)
when :helper_ivar then @_helper.instance_variable_get(ivar)
when :xml_markup then @builder.__send__(sym, *args, &block)
when :tag then tag!(sym, *args, &block)
when :tagset then @tagset.handle_tag sym, self, *args, &block
else super
end
end
def response_for sym
return :helper if @_helper.respond_to?(sym, true)
return :assigns if @assigns.has_key?(sym)
return :stringy_assigns if @assigns.has_key?(sym.to_s)
return :ivar if instance_variables_for(self).include?(ivar = "@#{sym}".to_sym)
return :helper_ivar if @_helper && instance_variables_for(@_helper).include?(ivar)
return :xml_markup if instance_methods_for(::Builder::XmlMarkup).include?(sym)
return :tag if @tagset.nil?
return :tagset if @tagset.can_handle? sym
nil
end
def respond_to_missing? sym, include_private = false
!response_for(sym).nil?
end
if RUBY_VERSION_ID >= 191
def instance_variables_for(obj)
obj.instance_variables
end
def instance_methods_for(obj)
obj.instance_methods
end
else
def instance_variables_for(obj)
obj.instance_variables.map { |var| var.to_sym }
end
def instance_methods_for(obj)
obj.instance_methods.map { |m| m.to_sym }
end
end
def fragment
stream = @streams.last
start = stream.length
yield
length = stream.length - start
Fragment.new(stream, start, length)
end
end
class Stream < Array
alias_method :to_s, :join
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 < BasicObject
def initialize(*args)
@stream, @start, @length = args
@transformed_stream = false
end
[:to_s, :inspect, :==].each do |method|
undef_method method if method_defined?(method)
end
def to_s
transform_stream unless transformed_stream?
@str.to_s
end
alias_method :to_str, :to_s
private
def method_missing(...)
transform_stream unless transformed_stream?
@str.__send__(...)
end
def respond_to_missing? sym, *args
@str.respond_to? sym
end
def transform_stream
@transformed_stream = true
# We can't do @stream.slice!(@start, @length),
# as it would invalidate the @starts and @lengths of other Fragment instances.
@str = @stream[@start, @length].join.to_s
@stream[@start, @length] = [nil] * @length
end
def transformed_stream?
@transformed_stream
end
end
class XmlMarkup < ::Builder::XmlMarkup
attr_accessor :target, :level
end
end