diff options
Diffstat (limited to 'activesupport')
-rw-r--r-- | activesupport/CHANGELOG | 15 | ||||
-rw-r--r-- | activesupport/Rakefile | 2 | ||||
-rw-r--r-- | activesupport/lib/active_support.rb | 5 | ||||
-rw-r--r-- | activesupport/lib/active_support/core_ext/array/conversions.rb | 7 | ||||
-rw-r--r-- | activesupport/lib/active_support/core_ext/hash.rb | 2 | ||||
-rw-r--r-- | activesupport/lib/active_support/core_ext/hash/conversions.rb | 39 | ||||
-rw-r--r-- | activesupport/lib/active_support/core_ext/string/inflections.rb | 4 | ||||
-rw-r--r-- | activesupport/lib/active_support/inflector.rb | 4 | ||||
-rw-r--r-- | activesupport/test/core_ext/array_ext_test.rb | 11 | ||||
-rw-r--r-- | activesupport/test/core_ext/hash_ext_test.rb | 30 | ||||
-rw-r--r-- | activesupport/test/inflector_test.rb | 12 | ||||
-rw-r--r-- | activesupport/vendor/builder.rb | 13 | ||||
-rw-r--r-- | activesupport/vendor/builder/blankslate.rb | 53 | ||||
-rw-r--r-- | activesupport/vendor/builder/xmlbase.rb | 143 | ||||
-rw-r--r-- | activesupport/vendor/builder/xmlevents.rb | 63 | ||||
-rw-r--r-- | activesupport/vendor/builder/xmlmarkup.rb | 308 |
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{&}, '&'). + gsub(%r{<}, '<'). + gsub(%r{>}, '>') + 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 & 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 <, + # > and & 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 |