# frozen_string_literal: true 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. # # === Namespace inheritance # # In the Builder context, children will inherit their parent's namespace. This is the same # behavior as if the underlying {XML::Document} set +namespace_inheritance+ to +true+: # # result = Nokogiri::XML::Builder.new do |xml| # xml["soapenv"].Envelope("xmlns:soapenv" => "http://schemas.xmlsoap.org/soap/envelope/") do # xml.Header # end # end # result.doc.to_xml # # => # # # # # # # # Users may turn this behavior off by passing a keyword argument +namespace_inheritance:false+ # to the initializer: # # result = Nokogiri::XML::Builder.new(namespace_inheritance: false) do |xml| # xml["soapenv"].Envelope("xmlns:soapenv" => "http://schemas.xmlsoap.org/soap/envelope/") do # xml.Header # xml["soapenv"].Body # users may explicitly opt into the namespace # end # end # result.doc.to_xml # # => # # # #
# # # # # # For more information on namespace inheritance, please see {XML::Document#namespace_inheritance} # # # == 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 DEFAULT_DOCUMENT_OPTIONS = {namespace_inheritance: true} # 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(File.read('somedoc.xml')) # Nokogiri::XML::Builder.with(doc.at_css('some_tag')) do |xml| # # ... Use normal builder methods here ... # xml.awesome # add the "awesome" tag below "some_tag" # end # def self.with(root, &block) 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 klassname = "::" + (self.class.name.split("::")[0..-2] + ["Document"]).join("::") klass = begin Object.const_get(klassname) rescue NameError Nokogiri::XML::Document end @parent = @doc = klass.new end @context = nil @arity = nil @ns = nil options = DEFAULT_DOCUMENT_OPTIONS.merge(options) 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 ### # Create a Comment Node with content of +string+ def comment(string) insert doc.create_comment(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) if @parent != @doc @ns = @parent.namespace_definitions.find { |x| x.prefix == ns.to_s } end 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 @ns = { :pending => ns.to_s } return self end ### # Convert this Builder object to XML def to_xml(*args) if Nokogiri.jruby? options = args.first.is_a?(Hash) ? args.shift : {} if !options[:save_with] options[:save_with] = Node::SaveOptions::AS_BUILDER end args.insert(0, options) end @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.is_a? Nokogiri::XML::Namespace n.namespace = @ns @ns = nil end } if @ns.is_a? Hash node.namespace = node.namespace_definitions.find { |x| x.prefix == @ns[:pending] } if node.namespace.nil? raise ArgumentError, "Namespace #{@ns[:pending]} has not been defined" end @ns = nil end insert(node, &block) end end private ### # Insert +node+ as a child of the current Node def insert(node, &block) node = @parent.add_child(node) 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