# The ElementBuilder Widget is the heart of RTML document generation. All other Widgets are built on top of this one, # and use it to formulate a valid document object model (DOM) representing the TML to be sent downstream. # # ElementBuilder can be invoked from any document or element via its entry point, #build. Any options are converted # into element properties. # # ElementBuilder will create an instance of Rtml::Dom::Element whose parent is whichever element you invoked the # ElementBuilder from and whose name is set accordingly. An error will be raised if the element does not match any # of the parent's valid children. # # A block can be passed to the ElementBuilder invocation in order to create children of the initially-built element. # In this case, a set of methods are predefined to match the names of valid children. Calling one of those methods # is exactly the same as calling #build with the first argument being the name of the element. # # Examples: # document = Rtml::Document.new # document.build(:tml) do # head # screen :id => 'gather_data' do # submit :target => url_for(params), :error => '#submit_err', :cache => "deny" do # getvar :name => 'terminal.datetime' # end # end # end # # It should be noted that generating a document using solely the ElementBuilder would be little different than writing # it in raw TML. This is discouraged. ElementBuilder is meant to serve as an interface upon which other Widgets can be # constructed. There's certainly no reason you can't use it to accomplish something the other Widgets do not, but you # might consider writing a brand-new Widget to accomplish the same task instead, so that others (and perhaps yourself # in the future) may benefit from your contribution of missing functionality. # # In the event that the method name that would be used to generate a child element is also the name of a Widget entry # point, that entry point takes priority over the generated method. # class Rtml::Widgets::ElementBuilder < Rtml::Widget affects :document, :element # all elements entry_point :build attr_reader :name disable_subprocessing! def rules_apply? parent.respond_to?(:document) && parent.document ? parent.document.rules : (parent.respond_to?(:rules) && parent.rules) end def build(name, properties = {}, &block) @name = name.to_s raise ArgumentError, "Expected properties to be a Hash", caller unless properties.kind_of?(Hash) skip_validation = properties.delete(:skip_validation) || false valid_tag_names = (rules_apply? ? case parent when Rtml::Document validate_tag_is_the_only_root unless skip_validation [ rules.root ] else validate_tag_is_in_order unless skip_validation rules.tag(parent.name).children end : []) validate_name_is_one_of(valid_tag_names) if rules_apply? and not skip_validation # validation passed, so create the element @element = parent.build_element(name, properties) if @element.respond_to?(:parent=) #if @element.changed? || @element.new_record? @element.parent = parent #else # @element.update_attribute(:parent, parent) #end end rebuild_invalid_elements add_method_proxies begin @element.process &block rescue # if the subprocessing failed, then remove the element from parent. if parent.respond_to?(:elements) parent.elements.delete @element elsif parent.respond_to?(:root) && parent.root == @element parent.root = nil end raise end if block_given? @element end private # Removes elements in the @out_of_order array from the parent element, and then rebuilds them. This ensures # that they come *after* the current element, because #validate_tag_is_in_order has already verified that that # is where they belong. def rebuild_invalid_elements @out_of_order.each do |o| ele = o parent.elements.delete o parent.elements << o end if @out_of_order end # Aliases #build in the newly-created element for each valid child of that element. def add_method_proxies if rules_apply? code = "" tag = rules.tag(name) proxy = Module.new entry_points = @element.widget_entry_points tag.children.each do |method_name| # we can't abort if respond_to? is true because that would prevent things like "display" or "p" from working # as expected. So we get a list of known entry points, and ignore them because anything we define here will # likely override them. (Since ElementBuilder is considerably low-level, that is probably the opposite of what # the user writing the Widget in question would expect.) Then we continue, even if the method already exists, # as long as it's not a Widget entry point. next if entry_points.include? method_name #puts "#{@element.name} - #{method_name}" code.concat "def #{method_name}(*a, &b); build('#{method_name}', *a, &b); end\n" end if tag && !tag.kind_of?(Array) proxy.class_eval code, __FILE__, __LINE__ unless code.blank? (class << @element; self; end).send(:include, proxy) end end # Raises RulesViolationError if #name is out of the accepted order. def validate_tag_is_in_order order = rules.order(parent.name) first = order.index(name) after = order.select { |i| order.index(i) > first } out_of_order = parent.elements.select { |c| after.include?(c.name) } # out_of_order is supposed to come after first, but is already in the list so is placed before it. # (first hasn't been created yet.) @out_of_order = out_of_order end # Raises RulesViolationError if #name is not a root tag or if there is already a root tag. def validate_tag_is_the_only_root unless name == rules.root raise Rtml::Errors::RulesViolationError, "Tag must be a root tag (#{rules.root.inspect}); was #{name.inspect}" end if (first = parent.root) raise Rtml::Errors::RulesViolationError, "Already have a root tag (#{first.name.inspect})" end end # Raises RulesViolationError if #name is not in the specified array. def validate_name_is_one_of(valid_tag_names) return if valid_tag_names.include? name raise Rtml::Errors::RulesViolationError, "Tag name should be one of #{valid_tag_names.inspect}; was #{name.inspect}" end end