diff --git a/README.md b/README.md
index d64345e..3542e3b 100644
--- a/README.md
+++ b/README.md
@@ -190,3 +190,63 @@ The example above shows an example of how you can use all three at the same time
Notice that we have the attribute "lang" defined twice.
The `@lang` value takes precedence over the `:attribute![:subtitle]["lang"]` value.
+
+## Pretty Print
+
+You can prettify the output XML to make it more readable. Use these options:
+* `pretty_print` – controls pretty mode (default: `false`)
+* `indent` – specifies indentation in spaces (default: `2`)
+* `compact` – controls compact mode (default: `true`)
+
+**This feature is not available for XML documents generated from arrays with unwrap option set to false as such documents are not valid**
+
+**Examples**
+
+``` ruby
+puts Gyoku.xml({user: { name: 'John', job: { title: 'Programmer' }, :@status => 'active' }}, pretty_print: true)
+#
+# John
+#
+# Programmer
+#
+#
+```
+
+``` ruby
+puts Gyoku.xml({user: { name: 'John', job: { title: 'Programmer' }, :@status => 'active' }}, pretty_print: true, indent: 4)
+#
+# John
+#
+# Programmer
+#
+#
+```
+
+``` ruby
+puts Gyoku.xml({user: { name: 'John', job: { title: 'Programmer' }, :@status => 'active' }}, pretty_print: true, compact: false)
+#
+#
+# John
+#
+#
+#
+# Programmer
+#
+#
+#
+```
+
+**Generate XML from an array with `unwrap` option set to `true`**
+``` ruby
+puts Gyoku::Array.to_xml(["john", "jane"], "user", true, {}, pretty_print: true, unwrap: true)
+#
+# john
+# jane
+#
+```
+
+**Generate XML from an array with `unwrap` option unset (`false` by default)**
+``` ruby
+puts Gyoku::Array.to_xml(["john", "jane"], "user", true, {}, pretty_print: true)
+#johnjane
+```
diff --git a/lib/gyoku/array.rb b/lib/gyoku/array.rb
index 46dd739..045d397 100644
--- a/lib/gyoku/array.rb
+++ b/lib/gyoku/array.rb
@@ -1,5 +1,6 @@
require "builder"
+require "gyoku/prettifier.rb"
require "gyoku/hash"
require "gyoku/xml_value"
@@ -8,9 +9,22 @@ class Array
NESTED_ELEMENT_NAME = "element"
+ # Builds XML and prettifies it if +pretty_print+ option is set to +true+
+ def self.to_xml(array, key, escape_xml = true, attributes = {}, options = {})
+ xml = build_xml(array, key, escape_xml, attributes, options)
+
+ if options[:pretty_print] && options[:unwrap]
+ Prettifier.prettify(xml, options)
+ else
+ xml
+ end
+ end
+
+ private
+
# Translates a given +array+ to XML. Accepts the XML +key+ to add the elements to,
# whether to +escape_xml+ and an optional Hash of +attributes+.
- def self.to_xml(array, key, escape_xml = true, attributes = {}, options = {})
+ def self.build_xml(array, key, escape_xml = true, attributes = {}, options = {})
self_closing = options.delete(:self_closing)
unwrap = options[:unwrap] || false
@@ -24,10 +38,10 @@ def self.to_xml(array, key, escape_xml = true, attributes = {}, options = {})
if unwrap
xml << Hash.to_xml(item, options)
else
- xml.tag!(key, attrs) { xml << Hash.to_xml(item, options) }
+ xml.tag!(key, attrs) { xml << Hash.build_xml(item, options) }
end
when ::Array then
- xml.tag!(key, attrs) { xml << Array.to_xml(item, NESTED_ELEMENT_NAME) }
+ xml.tag!(key, attrs) { xml << Array.build_xml(item, NESTED_ELEMENT_NAME) }
when NilClass then
xml.tag!(key, "xsi:nil" => "true")
else
@@ -37,8 +51,6 @@ def self.to_xml(array, key, escape_xml = true, attributes = {}, options = {})
end
end
- private
-
# Iterates over a given +array+ with a Hash of +attributes+ and yields a builder +xml+
# instance, the current +item+, any XML +attributes+ and the current +index+.
def self.iterate_with_xml(array, key, attributes, options, &block)
diff --git a/lib/gyoku/hash.rb b/lib/gyoku/hash.rb
index 9d7915e..ddd77f1 100644
--- a/lib/gyoku/hash.rb
+++ b/lib/gyoku/hash.rb
@@ -1,5 +1,6 @@
require "builder"
+require "gyoku/prettifier.rb"
require "gyoku/array"
require "gyoku/xml_key"
require "gyoku/xml_value"
@@ -7,8 +8,21 @@
module Gyoku
class Hash
- # Translates a given +hash+ with +options+ to XML.
+ # Builds XML and prettifies it if +pretty_print+ option is set to +true+
def self.to_xml(hash, options = {})
+ xml = build_xml(hash, options)
+
+ if options[:pretty_print]
+ Prettifier.prettify(xml, options)
+ else
+ xml
+ end
+ end
+
+ private
+
+ # Translates a given +hash+ with +options+ to XML.
+ def self.build_xml(hash, options = {})
iterate_with_xml hash do |xml, key, value, attributes|
self_closing = key.to_s[-1, 1] == "/"
escape_xml = key.to_s[-1, 1] != "!"
@@ -16,8 +30,8 @@ def self.to_xml(hash, options = {})
case
when :content! === key then xml << XMLValue.create(value, escape_xml, options)
- when ::Array === value then xml << Array.to_xml(value, xml_key, escape_xml, attributes, options.merge(:self_closing => self_closing))
- when ::Hash === value then xml.tag!(xml_key, attributes) { xml << Hash.to_xml(value, options) }
+ when ::Array === value then xml << Array.build_xml(value, xml_key, escape_xml, attributes, options.merge(:self_closing => self_closing))
+ when ::Hash === value then xml.tag!(xml_key, attributes) { xml << build_xml(value, options) }
when self_closing then xml.tag!(xml_key, attributes)
when NilClass === value then xml.tag!(xml_key, "xsi:nil" => "true")
else xml.tag!(xml_key, attributes) { xml << XMLValue.create(value, escape_xml, options) }
@@ -25,8 +39,6 @@ def self.to_xml(hash, options = {})
end
end
- private
-
# Iterates over a given +hash+ and yields a builder +xml+ instance, the current
# Hash +key+ and any XML +attributes+.
#
diff --git a/lib/gyoku/prettifier.rb b/lib/gyoku/prettifier.rb
new file mode 100644
index 0000000..3ca337d
--- /dev/null
+++ b/lib/gyoku/prettifier.rb
@@ -0,0 +1,29 @@
+require 'rexml/document'
+
+module Gyoku
+ class Prettifier
+ DEFAULT_INDENT = 2
+ DEFAULT_COMPACT = true
+
+ attr_accessor :indent, :compact
+
+ def self.prettify(xml, options = {})
+ new(options).prettify(xml)
+ end
+
+ def initialize(options = {})
+ @indent = options[:indent] || DEFAULT_INDENT
+ @compact = options[:compact].nil? ? DEFAULT_COMPACT : options[:compact]
+ end
+
+ # Adds intendations and newlines to +xml+ to make it more readable
+ def prettify(xml)
+ result = ''
+ formatter = REXML::Formatters::Pretty.new indent
+ formatter.compact = compact
+ doc = REXML::Document.new xml
+ formatter.write doc, result
+ result
+ end
+ end
+end
diff --git a/spec/gyoku/array_spec.rb b/spec/gyoku/array_spec.rb
index 5667274..4caff92 100644
--- a/spec/gyoku/array_spec.rb
+++ b/spec/gyoku/array_spec.rb
@@ -65,6 +65,44 @@
expect(to_xml(array, "value")).to eq(result)
end
+
+ context "when :pretty_print option is set to true" do
+ context "when :unwrap option is set to true" do
+ it "returns prettified xml" do
+ array = ["one", "two", {"three" => "four"}]
+ options = { pretty_print: true, unwrap: true }
+ result = "\n one\n two\n four\n"
+ expect(to_xml(array, "test", true, {}, options)).to eq(result)
+ end
+
+ context "when :indent option is specified" do
+ it "returns prettified xml with specified indent" do
+ array = ["one", "two", {"three" => "four"}]
+ options = { pretty_print: true, indent: 3, unwrap: true }
+ result = "\n one\n two\n four\n"
+ expect(to_xml(array, "test", true, {}, options)).to eq(result)
+ end
+ end
+
+ context "when :compact option is specified" do
+ it "returns prettified xml with specified compact mode" do
+ array = ["one", {"two" => "three"}]
+ options = { pretty_print: true, compact: false, unwrap: true }
+ result = "\n \n one\n \n \n three \n \n"
+ expect(to_xml(array, "test", true, {}, options)).to eq(result)
+ end
+ end
+ end
+
+ context "when :unwrap option is not set" do
+ it "returns non-prettified xml" do
+ array = ["one", "two", {"three" => "four"}]
+ options = { pretty_print: true }
+ result = "onetwofour"
+ expect(to_xml(array, "test", true, {}, options)).to eq(result)
+ end
+ end
+ end
end
def to_xml(*args)
diff --git a/spec/gyoku/hash_spec.rb b/spec/gyoku/hash_spec.rb
index 91fa958..3a877f5 100644
--- a/spec/gyoku/hash_spec.rb
+++ b/spec/gyoku/hash_spec.rb
@@ -52,6 +52,33 @@
expect(to_xml(:some => [{ :new => "user" }, { :old => "gorilla" }])).
to eq("usergorilla")
end
+
+ context "when :pretty_print option is set to true" do
+ it "returns prettified xml" do
+ hash = { some: { user: { name: "John", groups: ["admin", "editor"] } } }
+ options = { pretty_print: true }
+ result = "\n \n John\n admin\n editor\n \n"
+ expect(to_xml(hash, options)).to eq(result)
+ end
+
+ context "when :indent option is specified" do
+ it "returns prettified xml with specified indent" do
+ hash = { some: { user: { name: "John" } } }
+ options = { pretty_print: true, indent: 4 }
+ result = "\n \n John\n \n"
+ expect(to_xml(hash, options)).to eq(result)
+ end
+ end
+
+ context "when :compact option is specified" do
+ it "returns prettified xml with specified compact mode" do
+ hash = { some: { user: { name: "John" } } }
+ options = { pretty_print: true, compact: false }
+ result = "\n \n \n John\n \n \n"
+ expect(to_xml(hash, options)).to eq(result)
+ end
+ end
+ end
end
it "converts Hash key Symbols to lowerCamelCase" do
diff --git a/spec/gyoku/prettifier_spec.rb b/spec/gyoku/prettifier_spec.rb
new file mode 100644
index 0000000..467035e
--- /dev/null
+++ b/spec/gyoku/prettifier_spec.rb
@@ -0,0 +1,39 @@
+require "spec_helper"
+
+describe Gyoku::Prettifier do
+ describe "#prettify" do
+ context "when xml is valid" do
+ let!(:xml) { Gyoku::Hash.build_xml(test: { pretty: "xml" }) }
+
+ it "returns prettified xml" do
+ expect(subject.prettify(xml)).to eql("\n xml\n")
+ end
+
+ context "when indent option is specified" do
+ it "returns prettified xml with indent" do
+ options = { indent: 3 }
+ subject = Gyoku::Prettifier.new(options)
+ expect(subject.prettify(xml)).to eql("\n xml\n")
+ end
+ end
+
+ context "when compact option is specified" do
+ it "returns prettified xml with indent" do
+ options = { compact: false }
+ subject = Gyoku::Prettifier.new(options)
+ expect(subject.prettify(xml)).to eql("\n \n xml\n \n")
+ end
+ end
+ end
+
+ context "when xml is not valid" do
+ let!(:xml) do
+ Gyoku::Array.build_xml(["one", "two"], "test")
+ end
+
+ it "raises an error" do
+ expect{ subject.prettify(xml) }.to raise_error REXML::ParseException
+ end
+ end
+ end
+end