module Nokogiri module XML ### # Nokogiri builder can be used for building XML and HTML documents. # # == Synopsis: # # builder = Nokogiri::XML::Builder.new do |xml| # xml.root { # xml.products { # xml.widget { # xml.id_ "10" # xml.name "Awesome widget" # } # } # } # end # puts builder.to_xml # # Will output: # # # # # # 10 # Awesome widget # # # # # # === Builder scope # # The builder allows two forms. When the builder is supplied with a block # that has a parameter, the outside scope is maintained. This means you # can access variables that are outside your builder. If you don't need # outside scope, you can use the builder without the "xml" prefix like # this: # # builder = Nokogiri::XML::Builder.new do # root { # products { # widget { # id_ "10" # name "Awesome widget" # } # } # } # end # # == Special Tags # # The builder works by taking advantage of method_missing. Unfortunately # some methods are defined in ruby that are difficult or dangerous to # remove. You may want to create tags with the name "type", "class", and # "id" for example. In that case, you can use an underscore to # disambiguate your tag name from the method call. # # Here is an example of using the underscore to disambiguate tag names from # ruby methods: # # @objects = [Object.new, Object.new, Object.new] # # builder = Nokogiri::XML::Builder.new do |xml| # xml.root { # xml.objects { # @objects.each do |o| # xml.object { # xml.type_ o.type # xml.class_ o.class.name # xml.id_ o.id # } # end # } # } # end # puts builder.to_xml # # The underscore may be used with any tag name, and the last underscore # will just be removed. This code will output the following XML: # # # # # # Object # Object # 48390 # # # Object # Object # 48380 # # # Object # Object # 48370 # # # # # == Tag Attributes # # Tag attributes may be supplied as method arguments. Here is our # previous example, but using attributes rather than tags: # # @objects = [Object.new, Object.new, Object.new] # # builder = Nokogiri::XML::Builder.new do |xml| # xml.root { # xml.objects { # @objects.each do |o| # xml.object(:type => o.type, :class => o.class, :id => o.id) # end # } # } # end # puts builder.to_xml # # === Tag Attribute Short Cuts # # A couple attribute short cuts are available when building tags. The # short cuts are available by special method calls when building a tag. # # This example builds an "object" tag with the class attribute "classy" # and the id of "thing": # # builder = Nokogiri::XML::Builder.new do |xml| # xml.root { # xml.objects { # xml.object.classy.thing! # } # } # end # puts builder.to_xml # # Which will output: # # # # # # # # # All other options are still supported with this syntax, including # blocks and extra tag attributes. # # == Namespaces # # Namespaces are added similarly to attributes. Nokogiri::XML::Builder # assumes that when an attribute starts with "xmlns", it is meant to be # a namespace: # # builder = Nokogiri::XML::Builder.new { |xml| # xml.root('xmlns' => 'default', 'xmlns:foo' => 'bar') do # xml.tenderlove # end # } # puts builder.to_xml # # Will output XML like this: # # # # # # # === Referencing declared namespaces # # Tags that reference non-default namespaces (i.e. a tag "foo:bar") can be # built by using the Nokogiri::XML::Builder#[] method. # # For example: # # builder = Nokogiri::XML::Builder.new do |xml| # xml.root('xmlns:foo' => 'bar') { # xml.objects { # xml['foo'].object.classy.thing! # } # } # end # puts builder.to_xml # # Will output this XML: # # # # # # # # # Note the "foo:object" tag. # # == Document Types # # To create a document type (DTD), access use the Builder#doc method to get # the current context document. Then call Node#create_internal_subset to # create the DTD node. # # For example, this Ruby: # # builder = Nokogiri::XML::Builder.new do |xml| # xml.doc.create_internal_subset( # 'html', # "-//W3C//DTD HTML 4.01 Transitional//EN", # "http://www.w3.org/TR/html4/loose.dtd" # ) # xml.root do # xml.foo # end # end # # puts builder.to_xml # # Will output this xml: # # # # # # # class Builder # The current Document object being built attr_accessor :doc # The parent of the current node being built attr_accessor :parent # A context object for use when the block has no arguments attr_accessor :context attr_accessor :arity # :nodoc: ### # Create a builder with an existing root object. This is for use when # you have an existing document that you would like to augment with # builder methods. The builder context created will start with the # given +root+ node. # # For example: # # doc = Nokogiri::XML(open('somedoc.xml')) # Nokogiri::XML::Builder.with(doc.at('some_tag')) do |xml| # # ... Use normal builder methods here ... # xml.awesome # add the "awesome" tag below "some_tag" # end # def self.with root, &block builder = self.new({}, root, &block) end ### # Create a new Builder object. +options+ are sent to the top level # Document that is being built. # # Building a document with a particular encoding for example: # # Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml| # ... # end def initialize options = {}, root = nil, &block if root @doc = root.document @parent = root else namespace = self.class.name.split('::') namespace[-1] = 'Document' @doc = eval(namespace.join('::')).new @parent = @doc end @context = nil @arity = nil @ns = nil options.each do |k,v| @doc.send(:"#{k}=", v) end return unless block_given? @arity = block.arity if @arity <= 0 @context = eval('self', block.binding) instance_eval(&block) else yield self end @parent = @doc end ### # Create a Text Node with content of +string+ def text string insert @doc.create_text_node(string) end ### # Create a CDATA Node with content of +string+ def cdata string insert(doc.create_cdata(string)) end ### # Build a tag that is associated with namespace +ns+. Raises an # ArgumentError if +ns+ has not been defined higher in the tree. def [] ns @ns = @parent.namespace_definitions.find { |x| x.prefix == ns.to_s } return self if @ns @parent.ancestors.each do |a| next if a == doc @ns = a.namespace_definitions.find { |x| x.prefix == ns.to_s } return self if @ns end raise ArgumentError, "Namespace #{ns} has not been defined" end ### # Convert this Builder object to XML def to_xml(*args) @doc.to_xml(*args) end ### # Append the given raw XML +string+ to the document def << string @doc.fragment(string).children.each { |x| insert(x) } end def method_missing method, *args, &block # :nodoc: if @context && @context.respond_to?(method) @context.send(method, *args, &block) else node = @doc.create_element(method.to_s.sub(/[_!]$/, ''),*args) { |n| # Set up the namespace if @ns n.namespace = @ns @ns = nil end } insert(node, &block) end end private ### # Insert +node+ as a child of the current Node def insert(node, &block) node.parent = @parent if block_given? old_parent = @parent @parent = node @arity ||= block.arity if @arity <= 0 instance_eval(&block) else block.call(self) end @parent = old_parent end NodeBuilder.new(node, self) end class NodeBuilder # :nodoc: def initialize node, doc_builder @node = node @doc_builder = doc_builder end def []= k, v @node[k] = v end def [] k @node[k] end def method_missing(method, *args, &block) opts = args.last.is_a?(Hash) ? args.pop : {} case method.to_s when /^(.*)!$/ @node['id'] = $1 @node.content = args.first if args.first when /^(.*)=/ @node[$1] = args.first else @node['class'] = ((@node['class'] || '').split(/\s/) + [method.to_s]).join(' ') @node.content = args.first if args.first end # Assign any extra options opts.each do |k,v| @node[k.to_s] = ((@node[k.to_s] || '').split(/\s/) + [v]).join(' ') end if block_given? old_parent = @doc_builder.parent @doc_builder.parent = @node value = @doc_builder.instance_eval(&block) @doc_builder.parent = old_parent return value end self end end end end end