aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2006-03-08 02:56:25 +0000
committerDavid Heinemeier Hansson <david@loudthinking.com>2006-03-08 02:56:25 +0000
commitcd989472a5248aae4f827c35347bdb871de1822a (patch)
tree13cad5fa4cd877788747930fe6076fcd01085a0f /activesupport
parent24fca9d92ea9bef676f74259996c4039e014dfe2 (diff)
downloadrails-cd989472a5248aae4f827c35347bdb871de1822a.tar.gz
rails-cd989472a5248aae4f827c35347bdb871de1822a.tar.bz2
rails-cd989472a5248aae4f827c35347bdb871de1822a.zip
Added Hash#to_xml and Array#to_xml that makes it much easier to produce XML from basic structures [DHH] Moved Jim Weirich's wonderful Builder from Action Pack to Active Support (it's simply too useful to be stuck in AP) [DHH]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@3812 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'activesupport')
-rw-r--r--activesupport/CHANGELOG15
-rw-r--r--activesupport/Rakefile2
-rw-r--r--activesupport/lib/active_support.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/array/conversions.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/hash.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/hash/conversions.rb39
-rw-r--r--activesupport/lib/active_support/core_ext/string/inflections.rb4
-rw-r--r--activesupport/lib/active_support/inflector.rb4
-rw-r--r--activesupport/test/core_ext/array_ext_test.rb11
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb30
-rw-r--r--activesupport/test/inflector_test.rb12
-rw-r--r--activesupport/vendor/builder.rb13
-rw-r--r--activesupport/vendor/builder/blankslate.rb53
-rw-r--r--activesupport/vendor/builder/xmlbase.rb143
-rw-r--r--activesupport/vendor/builder/xmlevents.rb63
-rw-r--r--activesupport/vendor/builder/xmlmarkup.rb308
16 files changed, 706 insertions, 5 deletions
diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG
index 609d56ff68..12692c659c 100644
--- a/activesupport/CHANGELOG
+++ b/activesupport/CHANGELOG
@@ -1,5 +1,20 @@
*SVN*
+* Added Hash#to_xml and Array#to_xml that makes it much easier to produce XML from basic structures [DHH]. Examples:
+
+ { :name => "David", :street_name => "Paulina", :age => 26, :moved_on => Date.new(2005, 11, 15) }.to_xml
+
+ ...returns:
+
+ <person>
+ <street-name type="string">Paulina</street>
+ <name type="string">David</name>
+ <age type="integer">26</age>
+ <moved-on type="date">2005-11-15</moved-on>
+ </person>
+
+* Moved Jim Weirich's wonderful Builder from Action Pack to Active Support (it's simply too useful to be stuck in AP) [DHH]
+
* Fixed that Array#to_sentence will return "" on an empty array instead of ", and" #3842, #4031 [rubyonrails@beautifulpixel.com]
* Add Enumerable#group_by for grouping collections based on the result of some
diff --git a/activesupport/Rakefile b/activesupport/Rakefile
index d77a24f72b..74a8635a9c 100644
--- a/activesupport/Rakefile
+++ b/activesupport/Rakefile
@@ -18,7 +18,7 @@ task :default => :test
Rake::TestTask.new { |t|
t.pattern = 'test/**/*_test.rb'
t.verbose = true
- t.warning = true
+ t.warning = false
}
# Create compressed packages
diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb
index 7bd74101f5..a4c2462bdd 100644
--- a/activesupport/lib/active_support.rb
+++ b/activesupport/lib/active_support.rb
@@ -22,6 +22,9 @@
#++
$:.unshift(File.dirname(__FILE__))
+$:.unshift(File.dirname(__FILE__) + "/../vendor")
+
+require 'builder'
require 'active_support/inflector'
@@ -35,4 +38,4 @@ require 'active_support/option_merger'
require 'active_support/values/time_zone'
-require 'active_support/json'
+require 'active_support/json' \ No newline at end of file
diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb
index f119bb6c78..50777f9e1e 100644
--- a/activesupport/lib/active_support/core_ext/array/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/array/conversions.rb
@@ -1,7 +1,6 @@
module ActiveSupport #:nodoc:
module CoreExtensions #:nodoc:
module Array #:nodoc:
- # Enables to conversion of Arrays to human readable lists. ['one', 'two', 'three'] => "one, two, and three"
module Conversions
# Converts the array to comma-seperated sentence where the last element is joined by the connector word. Options:
# * <tt>:connector</tt>: The word used to join the last element in arrays with more than two elements (default: "and")
@@ -27,6 +26,12 @@ module ActiveSupport #:nodoc:
join '/'
end
+ def to_xml(options = {})
+ raise "Not all elements respond to to_xml" unless all? { |e| e.respond_to? :to_xml }
+ options[:root] ||= all? { |e| e.is_a? first.class } ? first.class.to_s.underscore.pluralize : "records"
+ xml = options[:builder] || Builder::XmlMarkup.new
+ xml.__send__(options[:root]) { each { |e| e.to_xml(:builder => xml) } }
+ end
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb
index f16368b18b..b8309baea3 100644
--- a/activesupport/lib/active_support/core_ext/hash.rb
+++ b/activesupport/lib/active_support/core_ext/hash.rb
@@ -1,9 +1,11 @@
require File.dirname(__FILE__) + '/hash/keys'
require File.dirname(__FILE__) + '/hash/indifferent_access'
require File.dirname(__FILE__) + '/hash/reverse_merge'
+require File.dirname(__FILE__) + '/hash/conversions'
class Hash #:nodoc:
include ActiveSupport::CoreExtensions::Hash::Keys
include ActiveSupport::CoreExtensions::Hash::IndifferentAccess
include ActiveSupport::CoreExtensions::Hash::ReverseMerge
+ include ActiveSupport::CoreExtensions::Hash::Conversions
end
diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb
new file mode 100644
index 0000000000..3218f6302c
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb
@@ -0,0 +1,39 @@
+module ActiveSupport #:nodoc:
+ module CoreExtensions #:nodoc:
+ module Hash #:nodoc:
+ module Conversions
+ XML_TYPE_NAMES = {
+ "String" => "string",
+ "Fixnum" => "integer",
+ "Date" => "date",
+ "Time" => "datetime"
+ }
+
+ XML_FORMATTING = {
+ "date" => Proc.new { |date| date.to_s(:db) },
+ "datetime" => Proc.new { |time| time.to_s(:db) }
+ }
+
+ def to_xml(options = {})
+ options.reverse_merge!({ :builder => Builder::XmlMarkup.new, :root => "hash" })
+
+ options[:builder].__send__(options[:root]) do
+ for key in keys
+ value = self[key]
+
+ if value.is_a?(self.class)
+ value.to_xml(:builder => options[:builder], :root => key)
+ else
+ type_name = XML_TYPE_NAMES[value.class.to_s]
+ options[:builder].__send__(key.to_s.dasherize,
+ XML_FORMATTING[type_name] ? XML_FORMATTING[type_name].call(value) : value,
+ value.nil? ? { } : { :type => type_name }
+ )
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb
index 5d19702719..eef8a5bc89 100644
--- a/activesupport/lib/active_support/core_ext/string/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/string/inflections.rb
@@ -26,6 +26,10 @@ module ActiveSupport #:nodoc:
Inflector.underscore(self)
end
+ def dasherize
+ Inflector.dasherize(self)
+ end
+
def demodulize
Inflector.demodulize(self)
end
diff --git a/activesupport/lib/active_support/inflector.rb b/activesupport/lib/active_support/inflector.rb
index f91a73c7f3..bbc5e10579 100644
--- a/activesupport/lib/active_support/inflector.rb
+++ b/activesupport/lib/active_support/inflector.rb
@@ -120,6 +120,10 @@ module Inflector
def underscore(camel_cased_word)
camel_cased_word.to_s.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase
end
+
+ def dasherize(underscored_word)
+ underscored_word.gsub(/_/, '-')
+ end
def humanize(lower_case_and_underscored_word)
lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb
index 266b5eefab..e13c57d262 100644
--- a/activesupport/test/core_ext/array_ext_test.rb
+++ b/activesupport/test/core_ext/array_ext_test.rb
@@ -67,5 +67,14 @@ class ArrayExtGroupingTests < Test::Unit::TestCase
assert_equal [%w(a b c), %w(d e f), ['g', false, false]], groups
end
-
end
+
+class ArraToXmlTests < Test::Unit::TestCase
+ def test_to_xml
+ a = [ { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" } ]
+ assert_equal(
+ "<hashes><hash><street-address type=\"string\">Paulina</street-address><name type=\"string\">David</name></hash><hash><street-address type=\"string\">Evergreen</street-address><name type=\"string\">Jason</name></hash></hashes>",
+ a.to_xml
+ )
+ end
+end \ No newline at end of file
diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb
index d74cea0991..b7d024f754 100644
--- a/activesupport/test/core_ext/hash_ext_test.rb
+++ b/activesupport/test/core_ext/hash_ext_test.rb
@@ -1,5 +1,5 @@
require 'test/unit'
-require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/hash'
+require File.dirname(__FILE__) + '/../../lib/active_support'
class HashExtTest < Test::Unit::TestCase
def setup
@@ -165,3 +165,31 @@ class HashExtTest < Test::Unit::TestCase
assert_equal({ :a => 1, :b => 2, :c => 10 }, { :a => 1, :b => 2 }.reverse_merge({:a => "x", :b => "y", :c => 10}) )
end
end
+
+class HashToXmlTest < Test::Unit::TestCase
+ def test_one_level
+ h = { :name => "David", :street => "Paulina" }
+ assert_equal %(<person><street type="string">Paulina</street><name type="string">David</name></person>), h.to_xml(:root => :person)
+ end
+
+ def test_one_level_with_types
+ h = { :name => "David", :street => "Paulina", :age => 26, :moved_on => Date.new(2005, 11, 15) }
+ assert_equal(
+ "<person><street type=\"string\">Paulina</street><name type=\"string\">David</name><age type=\"integer\">26</age><moved-on type=\"date\">2005-11-15</moved-on></person>",
+ h.to_xml(:root => :person)
+ )
+ end
+
+ def test_one_level_with_nils
+ h = { :name => "David", :street => "Paulina", :age => nil }
+ assert_equal(
+ "<person><street type=\"string\">Paulina</street><name type=\"string\">David</name><age></age></person>",
+ h.to_xml(:root => :person)
+ )
+ end
+
+ def test_two_levels
+ h = { :name => "David", :address => { :street => "Paulina" } }
+ assert_equal %(<person><address><street type="string">Paulina</street></address><name type="string">David</name></person>), h.to_xml(:root => :person)
+ end
+end \ No newline at end of file
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index c513abc967..d3f4a7727e 100644
--- a/activesupport/test/inflector_test.rb
+++ b/activesupport/test/inflector_test.rb
@@ -187,6 +187,12 @@ class InflectorTest < Test::Unit::TestCase
"1000" => "1000th",
"1001" => "1001st"
}
+
+ UnderscoresToDashes = {
+ "street" => "street",
+ "street_address" => "street-address",
+ "person_street_address" => "person-street-address"
+ }
def test_pluralize_plurals
assert_equal "plurals", Inflector.pluralize("plurals")
@@ -290,4 +296,10 @@ class InflectorTest < Test::Unit::TestCase
assert_equal(ordinalized, Inflector.ordinalize(number))
end
end
+
+ def test_dasherize
+ UnderscoresToDashes.each do |underscored, dasherized|
+ assert_equal(dasherized, Inflector.dasherize(underscored))
+ end
+ end
end
diff --git a/activesupport/vendor/builder.rb b/activesupport/vendor/builder.rb
new file mode 100644
index 0000000000..9719277669
--- /dev/null
+++ b/activesupport/vendor/builder.rb
@@ -0,0 +1,13 @@
+#!/usr/bin/env ruby
+
+#--
+# Copyright 2004 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.
+#++
+
+require 'builder/xmlmarkup'
+require 'builder/xmlevents'
diff --git a/activesupport/vendor/builder/blankslate.rb b/activesupport/vendor/builder/blankslate.rb
new file mode 100644
index 0000000000..1408c872cc
--- /dev/null
+++ b/activesupport/vendor/builder/blankslate.rb
@@ -0,0 +1,53 @@
+#!/usr/bin/env ruby
+#--
+# Copyright 2004 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.
+#++
+
+module Builder #:nodoc:
+
+ # BlankSlate provides an abstract base class with no predefined
+ # methods (except for <tt>\_\_send__</tt> and <tt>\_\_id__</tt>).
+ # BlankSlate is useful as a base class when writing classes that
+ # depend upon <tt>method_missing</tt> (e.g. dynamic proxies).
+ class BlankSlate #:nodoc:
+ class << self
+ def hide(name)
+ undef_method name if
+ instance_methods.include?(name.to_s) and
+ name !~ /^(__|instance_eval)/
+ end
+ end
+
+ instance_methods.each { |m| hide(m) }
+ end
+end
+
+# Since Ruby is very dynamic, methods added to the ancestors of
+# BlankSlate <em>after BlankSlate is defined</em> will show up in the
+# list of available BlankSlate methods. We handle this by defining a hook in the Object and Kernel classes that will hide any defined
+module Kernel #:nodoc:
+ class << self
+ alias_method :blank_slate_method_added, :method_added
+ def method_added(name)
+ blank_slate_method_added(name)
+ return if self != Kernel
+ Builder::BlankSlate.hide(name)
+ end
+ end
+end
+
+class Object #:nodoc:
+ class << self
+ alias_method :blank_slate_method_added, :method_added
+ def method_added(name)
+ blank_slate_method_added(name)
+ return if self != Object
+ Builder::BlankSlate.hide(name)
+ end
+ end
+end
diff --git a/activesupport/vendor/builder/xmlbase.rb b/activesupport/vendor/builder/xmlbase.rb
new file mode 100644
index 0000000000..7202bb2ead
--- /dev/null
+++ b/activesupport/vendor/builder/xmlbase.rb
@@ -0,0 +1,143 @@
+#!/usr/bin/env ruby
+
+require 'builder/blankslate'
+
+module Builder #:nodoc:
+
+ # Generic error for builder
+ class IllegalBlockError < RuntimeError #:nodoc:
+ end
+
+ # XmlBase is a base class for building XML builders. See
+ # Builder::XmlMarkup and Builder::XmlEvents for examples.
+ class XmlBase < BlankSlate #:nodoc:
+
+ # Create an XML markup builder.
+ #
+ # out:: Object receiving the markup.1 +out+ must respond to
+ # <tt><<</tt>.
+ # indent:: Number of spaces used for indentation (0 implies no
+ # indentation and no line breaks).
+ # initial:: Level of initial indentation.
+ #
+ def initialize(indent=0, initial=0)
+ @indent = indent
+ @level = initial
+ end
+
+ # Create a tag named +sym+. Other than the first argument which
+ # is the tag name, the arguements are the same as the tags
+ # implemented via <tt>method_missing</tt>.
+ def tag!(sym, *args, &block)
+ self.__send__(sym, *args, &block)
+ end
+
+ # Create XML markup based on the name of the method. This method
+ # is never invoked directly, but is called for each markup method
+ # in the markup block.
+ def method_missing(sym, *args, &block)
+ text = nil
+ attrs = nil
+ sym = "#{sym}:#{args.shift}" if args.first.kind_of?(Symbol)
+ args.each do |arg|
+ case arg
+ when Hash
+ attrs ||= {}
+ attrs.merge!(arg)
+ else
+ text ||= ''
+ text << arg.to_s
+ end
+ end
+ if block
+ unless text.nil?
+ raise ArgumentError, "XmlMarkup cannot mix a text argument with a block"
+ end
+ _capture_outer_self(block) if @self.nil?
+ _indent
+ _start_tag(sym, attrs)
+ _newline
+ _nested_structures(block)
+ _indent
+ _end_tag(sym)
+ _newline
+ elsif text.nil?
+ _indent
+ _start_tag(sym, attrs, true)
+ _newline
+ else
+ _indent
+ _start_tag(sym, attrs)
+ text! text
+ _end_tag(sym)
+ _newline
+ end
+ @target
+ end
+
+ # Append text to the output target. Escape any markup. May be
+ # used within the markup brackets as:
+ #
+ # builder.p { br; text! "HI" } #=> <p><br/>HI</p>
+ def text!(text)
+ _text(_escape(text))
+ end
+
+ # Append text to the output target without escaping any markup.
+ # May be used within the markup brackets as:
+ #
+ # builder.p { |x| x << "<br/>HI" } #=> <p><br/>HI</p>
+ #
+ # This is useful when using non-builder enabled software that
+ # generates strings. Just insert the string directly into the
+ # builder without changing the inserted markup.
+ #
+ # It is also useful for stacking builder objects. Builders only
+ # use <tt><<</tt> to append to the target, so by supporting this
+ # method/operation builders can use other builders as their
+ # targets.
+ def <<(text)
+ _text(text)
+ end
+
+ # For some reason, nil? is sent to the XmlMarkup object. If nil?
+ # is not defined and method_missing is invoked, some strange kind
+ # of recursion happens. Since nil? won't ever be an XML tag, it
+ # is pretty safe to define it here. (Note: this is an example of
+ # cargo cult programming,
+ # cf. http://fishbowl.pastiche.org/2004/10/13/cargo_cult_programming).
+ def nil?
+ false
+ end
+
+ private
+
+ def _escape(text)
+ text.
+ gsub(%r{&}, '&amp;').
+ gsub(%r{<}, '&lt;').
+ gsub(%r{>}, '&gt;')
+ end
+
+ def _capture_outer_self(block)
+ @self = eval("self", block)
+ end
+
+ def _newline
+ return if @indent == 0
+ text! "\n"
+ end
+
+ def _indent
+ return if @indent == 0 || @level == 0
+ text!(" " * (@level * @indent))
+ end
+
+ def _nested_structures(block)
+ @level += 1
+ block.call(self)
+ ensure
+ @level -= 1
+ end
+ end
+end
diff --git a/activesupport/vendor/builder/xmlevents.rb b/activesupport/vendor/builder/xmlevents.rb
new file mode 100644
index 0000000000..15dc7b6421
--- /dev/null
+++ b/activesupport/vendor/builder/xmlevents.rb
@@ -0,0 +1,63 @@
+#!/usr/bin/env ruby
+
+#--
+# Copyright 2004 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.
+#++
+
+require 'builder/xmlmarkup'
+
+module Builder
+
+ # Create a series of SAX-like XML events (e.g. start_tag, end_tag)
+ # from the markup code. XmlEvent objects are used in a way similar
+ # to XmlMarkup objects, except that a series of events are generated
+ # and passed to a handler rather than generating character-based
+ # markup.
+ #
+ # Usage:
+ # xe = Builder::XmlEvents.new(hander)
+ # xe.title("HI") # Sends start_tag/end_tag/text messages to the handler.
+ #
+ # Indentation may also be selected by providing value for the
+ # indentation size and initial indentation level.
+ #
+ # xe = Builder::XmlEvents.new(handler, indent_size, initial_indent_level)
+ #
+ # == XML Event Handler
+ #
+ # The handler object must expect the following events.
+ #
+ # [<tt>start_tag(tag, attrs)</tt>]
+ # Announces that a new tag has been found. +tag+ is the name of
+ # the tag and +attrs+ is a hash of attributes for the tag.
+ #
+ # [<tt>end_tag(tag)</tt>]
+ # Announces that an end tag for +tag+ has been found.
+ #
+ # [<tt>text(text)</tt>]
+ # Announces that a string of characters (+text+) has been found.
+ # A series of characters may be broken up into more than one
+ # +text+ call, so the client cannot assume that a single
+ # callback contains all the text data.
+ #
+ class XmlEvents < XmlMarkup #:nodoc:
+ def text!(text)
+ @target.text(text)
+ end
+
+ def _start_tag(sym, attrs, end_too=false)
+ @target.start_tag(sym, attrs)
+ _end_tag(sym) if end_too
+ end
+
+ def _end_tag(sym)
+ @target.end_tag(sym)
+ end
+ end
+
+end
diff --git a/activesupport/vendor/builder/xmlmarkup.rb b/activesupport/vendor/builder/xmlmarkup.rb
new file mode 100644
index 0000000000..b7e3b2d009
--- /dev/null
+++ b/activesupport/vendor/builder/xmlmarkup.rb
@@ -0,0 +1,308 @@
+#!/usr/bin/env ruby
+#--
+# Copyright 2004 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") # => <em>emphasized</em>
+ # xm.em { xmm.b("emp & bold") } # => <em><b>emph &amp; bold</b></em>
+ # xm.a("A Link", "href"=>"http://onestepback.org")
+ # # => <a href="http://onestepback.org">A Link</a>
+ # xm.div { br } # => <div><br/></div>
+ # xm.target("name"=>"compile", "option"=>"fast")
+ # # => <target option="fast" name="compile"\>
+ # # NOTE: order of attributes is not specified.
+ #
+ # xm.instruct! # <?xml version="1.0" encoding="UTF-8"?>
+ # xm.html { # <html>
+ # xm.head { # <head>
+ # xm.title("History") # <title>History</title>
+ # } # </head>
+ # xm.body { # <body>
+ # xm.comment! "HI" # <!-- HI -->
+ # xm.h1("Header") # <h1>Header</h1>
+ # xm.p("paragraph") # <p>paragraph</p>
+ # } # </body>
+ # } # </html>
+ #
+ # == Notes:
+ #
+ # * The order that attributes are inserted in markup tags is
+ # undefined.
+ #
+ # * Sometimes you wish to insert text without enclosing tags. Use
+ # the <tt>text!</tt> method to accomplish this.
+ #
+ # Example:
+ #
+ # xm.div { # <div>
+ # xm.text! "line"; xm.br # line<br/>
+ # xm.text! "another line"; xmbr # another line<br/>
+ # } # </div>
+ #
+ # * The special XML characters <, >, and & are converted to &lt;,
+ # &gt; and &amp; automatically. Use the <tt><<</tt> operation to
+ # insert text without modification.
+ #
+ # * Sometimes tags use special characters not allowed in ruby
+ # identifiers. Use the <tt>tag!</tt> method to handle these
+ # cases.
+ #
+ # Example:
+ #
+ # xml.tag!("SOAP:Envelope") { ... }
+ #
+ # will produce ...
+ #
+ # <SOAP:Envelope> ... </SOAP:Envelope>"
+ #
+ # <tt>tag!</tt> 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. "<tt>SOAP:Envelope</tt>" =>
+ # "<tt>xml.SOAP :Envelope</tt>")
+ #
+ # * XmlMarkup builds the markup in any object (called a _target_)
+ # that accepts the <tt><<</tt> 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(:ident=>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:
+ # # <name>
+ # # <first>Jim</first>
+ # # <last>Weirich</last>
+ # # </name>
+ #
+ # * 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=><em>target_object</em>::
+ # Object receiving the markup. +out+ must respond to the
+ # <tt><<</tt> operator. The default is a plain string target.
+ # :indent=><em>indentation</em>::
+ # Number of spaces used for indentation. The default is no
+ # indentation and no line breaks.
+ # :margin=><em>initial_indentation_level</em>::
+ # Amount of initial indentation (specified in levels, not
+ # spaces).
+ #
+ 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"
+ # # => <!ELEMENT blah "yada">
+ def declare!(inst, *args, &block)
+ _indent
+ @target << "<!#{inst}"
+ args.each do |arg|
+ case arg
+ when String
+ @target << %{ "#{arg}"}
+ when Symbol
+ @target << " #{arg}"
+ end
+ end
+ if block_given?
+ @target << " ["
+ _newline
+ _nested_structures(block)
+ @target << "]"
+ end
+ @target << ">"
+ _newline
+ end
+
+ # Insert a processing instruction into the XML markup. E.g.
+ #
+ # For example:
+ #
+ # xml.instruct!
+ # #=> <?xml version="1.0" encoding="UTF-8"?>
+ # xml.instruct! :aaa, :bbb=>"ccc"
+ # #=> <?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(
+ "<?#{directive_tag}",
+ "?>",
+ nil,
+ attrs,
+ [:version, :encoding, :standalone])
+ end
+
+ # Surrounds the given text with a CDATA tag
+ #
+ # For example:
+ #
+ # xml.cdata! "blah blah blah"
+ # # => <![CDATA[blah blah blah]]>
+ def cdata!(text)
+ _ensure_no_block block_given?
+ _special("<![CDATA[", "]]>", 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 <tt>end_too</tt> is true, then the start
+ # tag is also the end tag (e.g. <br/>
+ 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 << "</#{sym}>"
+ 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}="#{v}"} if v
+ end
+ attrs.each do |k, v|
+ @target << %{ #{k}="#{v}"} unless order.member?(k)
+ end
+ end
+
+ def _ensure_no_block(got_block)
+ if got_block
+ fail IllegalBlockError,
+ "Blocks are not allowed on XML instructions"
+ end
+ end
+
+ end
+
+end