#!/usr/bin/env ruby #-- # Copyright 2004, 2005 by Jim Weirich (jim@weirichhouse.org). # All rights reserved. # Permission is granted for use, copying, modification, distribution, # and distribution of modified versions of this work as long as the # above copyright notice is included. #++ # Provide a flexible and easy to use Builder for creating XML markup. # See XmlBuilder for usage details. require 'builder/xmlbase' module Builder # Create XML markup easily. All (well, almost all) methods sent to # an XmlMarkup object will be translated to the equivalent XML # markup. Any method with a block will be treated as an XML markup # tag with nested markup in the block. # # Examples will demonstrate this easier than words. In the # following, +xm+ is an +XmlMarkup+ object. # # xm.em("emphasized") # => emphasized # xm.em { xmm.b("emp & bold") } # => emph & bold # xm.a("A Link", "href"=>"http://onestepback.org") # # => A Link # xm.div { br } # =>

# xm.target("name"=>"compile", "option"=>"fast") # # => # # NOTE: order of attributes is not specified. # # xm.instruct! # # xm.html { # # xm.head { # # xm.title("History") # History # } # # xm.body { # # xm.comment! "HI" # # xm.h1("Header") #

Header

# xm.p("paragraph") #

paragraph

# } # # } # # # == Notes: # # * The order that attributes are inserted in markup tags is # undefined. # # * Sometimes you wish to insert text without enclosing tags. Use # the text! method to accomplish this. # # Example: # # xm.div { #
# xm.text! "line"; xm.br # line
# xm.text! "another line"; xmbr # another line
# } #
# # * The special XML characters <, >, and & are converted to <, # > and & automatically. Use the << operation to # insert text without modification. # # * Sometimes tags use special characters not allowed in ruby # identifiers. Use the tag! method to handle these # cases. # # Example: # # xml.tag!("SOAP:Envelope") { ... } # # will produce ... # # ... " # # tag! will also take text and attribute arguments (after # the tag name) like normal markup methods. (But see the next # bullet item for a better way to handle XML namespaces). # # * Direct support for XML namespaces is now available. If the # first argument to a tag call is a symbol, it will be joined to # the tag to produce a namespace:tag combination. It is easier to # show this than describe it. # # xml.SOAP :Envelope do ... end # # Just put a space before the colon in a namespace to produce the # right form for builder (e.g. "SOAP:Envelope" => # "xml.SOAP :Envelope") # # * XmlMarkup builds the markup in any object (called a _target_) # that accepts the << method. If no target is given, # then XmlMarkup defaults to a string target. # # Examples: # # xm = Builder::XmlMarkup.new # result = xm.title("yada") # # result is a string containing the markup. # # buffer = "" # xm = Builder::XmlMarkup.new(buffer) # # The markup is appended to buffer (using <<) # # xm = Builder::XmlMarkup.new(STDOUT) # # The markup is written to STDOUT (using <<) # # xm = Builder::XmlMarkup.new # x2 = Builder::XmlMarkup.new(:target=>xm) # # Markup written to +x2+ will be send to +xm+. # # * Indentation is enabled by providing the number of spaces to # indent for each level as a second argument to XmlBuilder.new. # Initial indentation may be specified using a third parameter. # # Example: # # xm = Builder.new(:indent=>2) # # xm will produce nicely formatted and indented XML. # # xm = Builder.new(:indent=>2, :margin=>4) # # xm will produce nicely formatted and indented XML with 2 # # spaces per indent and an over all indentation level of 4. # # builder = Builder::XmlMarkup.new(:target=>$stdout, :indent=>2) # builder.name { |b| b.first("Jim"); b.last("Weirich) } # # prints: # # # # Jim # # Weirich # # # # * The instance_eval implementation which forces self to refer to # the message receiver as self is now obsolete. We now use normal # block calls to execute the markup block. This means that all # markup methods must now be explicitly send to the xml builder. # For instance, instead of # # xml.div { strong("text") } # # you need to write: # # xml.div { xml.strong("text") } # # Although more verbose, the subtle change in semantics within the # block was found to be prone to error. To make this change a # little less cumbersome, the markup block now gets the markup # object sent as an argument, allowing you to use a shorter alias # within the block. # # For example: # # xml_builder = Builder::XmlMarkup.new # xml_builder.div { |xml| # xml.stong("text") # } # class XmlMarkup < XmlBase # Create an XML markup builder. Parameters are specified by an # option hash. # # :target=>target_object:: # Object receiving the markup. +out+ must respond to the # << operator. The default is a plain string target. # # :indent=>indentation:: # Number of spaces used for indentation. The default is no # indentation and no line breaks. # # :margin=>initial_indentation_level:: # Amount of initial indentation (specified in levels, not # spaces). # # :escape_attrs=>OBSOLETE:: # The :escape_attrs option is no longer supported by builder # (and will be quietly ignored). String attribute values are # now automatically escaped. If you need unescaped attribute # values (perhaps you are using entities in the attribute # values), then give the value as a Symbol. This allows much # finer control over escaping attribute values. # def initialize(options={}) indent = options[:indent] || 0 margin = options[:margin] || 0 super(indent, margin) @target = options[:target] || "" end # Return the target of the builder. def target! @target end def comment!(comment_text) _ensure_no_block block_given? _special("", comment_text, nil) end # Insert an XML declaration into the XML markup. # # For example: # # xml.declare! :ELEMENT, :blah, "yada" # # => def declare!(inst, *args, &block) _indent @target << "" _newline end # Insert a processing instruction into the XML markup. E.g. # # For example: # # xml.instruct! # #=> # xml.instruct! :aaa, :bbb=>"ccc" # #=> # def instruct!(directive_tag=:xml, attrs={}) _ensure_no_block block_given? if directive_tag == :xml a = { :version=>"1.0", :encoding=>"UTF-8" } attrs = a.merge attrs end _special( "", nil, attrs, [:version, :encoding, :standalone]) end # Insert a CDATA section into the XML markup. # # For example: # # xml.cdata!("text to be included in cdata") # #=> # def cdata!(text) _ensure_no_block block_given? _special("", text, nil) end private # NOTE: All private methods of a builder object are prefixed when # a "_" character to avoid possible conflict with XML tag names. # Insert text directly in to the builder's target. def _text(text) @target << text end # Insert special instruction. def _special(open, close, data=nil, attrs=nil, order=[]) _indent @target << open @target << data if data _insert_attributes(attrs, order) if attrs @target << close _newline end # Start an XML tag. If end_too is true, then the start # tag is also the end tag (e.g.
def _start_tag(sym, attrs, end_too=false) @target << "<#{sym}" _insert_attributes(attrs) @target << "/" if end_too @target << ">" end # Insert an ending tag. def _end_tag(sym) @target << "" end # Insert the attributes (given in the hash). def _insert_attributes(attrs, order=[]) return if attrs.nil? order.each do |k| v = attrs[k] @target << %{ #{k}="#{_attr_value(v)}"} if v # " WART end attrs.each do |k, v| @target << %{ #{k}="#{_attr_value(v)}"} unless order.member?(k) # " WART end end def _attr_value(value) case value when Symbol value.to_s else _escape_quote(value.to_s) end end def _ensure_no_block(got_block) if got_block fail IllegalBlockError, "Blocks are not allowed on XML instructions" end end end end