stead 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2009 North Carolina State University
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README.rdoc ADDED
@@ -0,0 +1,75 @@
1
+ = stead
2
+
3
+ Spreadsheets To Encoded Archival Description. Turns CSV files of container lists
4
+ into a stub EAD XML record.
5
+
6
+ == Story
7
+
8
+ Sometimes donors have spreadsheets which list the contents of their collections.
9
+ Rather than retype all of these into Archivists' Toolkit or an XML editor,
10
+ wouldn't it be nice to automatically generate a stub EAD XML document from the
11
+ spreadsheet?
12
+
13
+ With Stead you can. Just edit the headers (first row of the spreadsheet) to
14
+ conform to the Stead schema. This may involve splitting some columns to conform
15
+ to the schema, adding columns, and other editing. All of this is likely easier,
16
+ faster and more accurate to do in a spreadsheet than trying to do it elsewhere
17
+ retyping the whole thing.
18
+
19
+ Once the spreadsheet is ready just save it as a CSV and use the commandline tool
20
+ csv2ead to output an EAD XML document. Import into Archivists' Toolkit.
21
+
22
+ == Requirements
23
+
24
+ Ruby
25
+
26
+ == Examples that follow the schema
27
+
28
+ Look in test/contianer_lists/ at the following good examples of the CSV schema:
29
+ mc00000_container_list.csv
30
+ mc00000_container_list_no_series.csv
31
+ The order of the columns does not matter, but the headings must be exactly the
32
+ same case and spaces as those found in these files.
33
+
34
+ == Instructions
35
+
36
+ Once you have your spreadsheet in the correct schema, do the following:
37
+ - Save the spreadsheet as a CSV file.
38
+ - csv2ead --help for current commandline options.
39
+
40
+ = Stead::Extra
41
+
42
+ From the commandline you can specify a Stead::Extra class which will be required.
43
+ This class must define a Stead::Extra.run method which accepts an ead and eadid,
44
+ creates a new Stead::Extra object and then does any further processing you'd
45
+ like. See examples/ncsu.rb.
46
+
47
+ == Support
48
+
49
+ Please let me know what else you need in such a tool and I'll try to work it in.
50
+
51
+ == Limitations
52
+
53
+ - Some of this is still be NCSU and Archivists' Toolkit specific.
54
+ - This tool has only been used a handful of times so far.
55
+ - Only works with this specific schema.
56
+ - Only known to work with series at the c01 level and files at the c02 level.
57
+ Other deeper levels of nesting will not currently work. ()May work with subseries.)
58
+ - Column values like series must be duplicated for each row.
59
+
60
+ == TODO
61
+
62
+ - More tests (though there are already lots of tests).
63
+ - Better documentation on the CSV file schema.
64
+ - Rdoc.
65
+ - Automate tests of csv2ead tool.
66
+ - Expand the schema to other parts of the EAD?
67
+
68
+ == Author
69
+
70
+ Jason Ronallo
71
+
72
+ == Copyright
73
+
74
+ Copyright (c) 2010 North Carolina State University. See LICENSE for details.
75
+
data/Rakefile ADDED
@@ -0,0 +1,72 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "stead"
8
+ gem.summary = %Q{Spreadsheets To Encoded Archival Description}
9
+ gem.description = %Q{Converts CSV files of a specific schema into EAD XML.}
10
+ gem.email = "jronallo@gmail.com"
11
+ gem.homepage = "http://github.com/jronallo/stead"
12
+ gem.authors = ["Jason Ronallo"]
13
+ gem.add_dependency "nokogiri", ">= 1.4.1"
14
+ gem.add_dependency "fastercsv", ">= 1.5.0"
15
+ gem.add_dependency "activesupport", ">= 2.3.5"
16
+ gem.add_dependency "trollop", ">= 1.16.2"
17
+ gem.add_development_dependency "shoulda", ">= 0"
18
+ gem.files = FileList["[A-Z]*", "{bin,examples,lib}/**/*"]
19
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
24
+ end
25
+
26
+ require 'rake/testtask'
27
+ Rake::TestTask.new(:test) do |test|
28
+ test.libs << 'lib' << 'test'
29
+ test.pattern = 'test/**/test_*.rb'
30
+ test.verbose = true
31
+ end
32
+
33
+ begin
34
+ require 'rcov/rcovtask'
35
+ Rcov::RcovTask.new do |test|
36
+ test.libs << 'test'
37
+ test.pattern = 'test/**/test_*.rb'
38
+ test.verbose = true
39
+ end
40
+ rescue LoadError
41
+ task :rcov do
42
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
43
+ end
44
+ end
45
+
46
+ task :test => :check_dependencies
47
+
48
+ begin
49
+ require 'reek/adapters/rake_task'
50
+ Reek::RakeTask.new do |t|
51
+ t.fail_on_error = true
52
+ t.verbose = false
53
+ t.source_files = 'lib/**/*.rb'
54
+ end
55
+ rescue LoadError
56
+ task :reek do
57
+ abort "Reek is not available. In order to run reek, you must: sudo gem install reek"
58
+ end
59
+ end
60
+
61
+ task :default => :test
62
+
63
+ require 'rake/rdoctask'
64
+ Rake::RDocTask.new do |rdoc|
65
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
66
+
67
+ rdoc.rdoc_dir = 'rdoc'
68
+ rdoc.title = "stead #{version}"
69
+ rdoc.rdoc_files.include('README*')
70
+ rdoc.rdoc_files.include('lib/**/*.rb')
71
+ end
72
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.2
data/bin/csv2ead ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+ require 'pp'
4
+ require 'stead'
5
+ require 'trollop'
6
+
7
+ opts = Trollop::options do
8
+ banner <<-EOS
9
+ This script takes a csv file with a name in the format <eadid>_container_list.csv
10
+ and creates a stub EAD XML document.
11
+
12
+ Usage:
13
+ csv2ead --csv /path/to/<eadid>_container_list.csv [options]
14
+
15
+ where options are:
16
+ EOS
17
+
18
+ opt :csv, "A CSV file", :required => true, :type => String
19
+ opt :baseurl, 'Base URL for adding on the eadid', :type => String
20
+ opt :url, 'Full URL for this collection guide', :type => String
21
+ opt :template, 'Specify using a different EAD XML template', :type => String
22
+ opt :ncsu, 'Use NCSU specific template'
23
+ opt :extra, 'Full path to a Stead::Extra file to add in other data', :type => String
24
+ opt :output, 'Save the file by specifying the filename', :type => String
25
+ opt :pretty, 'If --output is specified this will pretty indent the container list.'
26
+ opt :stdout, 'Output full EAD to terminal'
27
+ end
28
+
29
+ unless opts[:output] or opts[:stdout]
30
+ puts "You must specify either --output <file> and/or --stdout to direct output to the terminal."
31
+ exit
32
+ end
33
+
34
+ if opts[:ncsu]
35
+ opts[:template] = File.join(File.dirname(__FILE__), '..', 'lib', 'stead', 'templates', 'ncsu_ead.xml')
36
+ opts[:baseurl] = 'http://www.lib.ncsu.edu/findingaids'
37
+ opts[:extra] = File.join(File.dirname(__FILE__), '..', 'examples', 'ncsu.rb')
38
+ end
39
+
40
+ ead_options = {}
41
+ # add eadid from filename
42
+ # basename will include _container_list so we need to remove that
43
+ basename = File.basename(opts[:csv], '.csv')
44
+ ead_options[:eadid] = basename.sub(/_container_list.*$/, '')
45
+ ead_options[:base_url] = opts[:baseurl] if opts[:baseurl]
46
+ [:template, :url].each do |key|
47
+ ead_options[key] = opts[key] if opts[key]
48
+ end
49
+
50
+ ead_generator = Stead::EadGenerator.from_csv(File.read(opts[:csv]), ead_options)
51
+ ead = ead_generator.to_ead
52
+
53
+ # add any extra content or elements to the EAD before outputting
54
+ if opts[:extra]
55
+ require opts[:extra]
56
+ Stead::Extra.run(ead, ead_options[:eadid])
57
+ end
58
+
59
+ if opts[:output]
60
+ File.open(opts[:output], 'w') do |fh|
61
+ if opts[:pretty]
62
+ fh.puts Stead.pretty_write(ead)
63
+ else
64
+ fh.puts ead
65
+ end
66
+ end
67
+ end
68
+
69
+ puts Stead.pretty_write(ead) if opts[:stdout]
70
+
data/examples/ncsu.rb ADDED
@@ -0,0 +1,74 @@
1
+ module Stead
2
+ class Extra
3
+ attr_accessor :ead, :eadid
4
+
5
+ def initialize(ead,eadid)
6
+ @ead = ead
7
+ @eadid = eadid
8
+ end
9
+
10
+ def self.run(ead, eadid)
11
+ extra = self.new(ead,eadid)
12
+ extra.add_collection_specific
13
+ ead
14
+ end
15
+
16
+ def add_collection_specific
17
+ if eadid.include?('ua')
18
+ # add additional conditions governing use note
19
+ add_ua_userestrict(ead)
20
+ append_to_titleproper(ead, eadid, 'Records')
21
+ archdesc_level(ead, 'subgrp')
22
+ elsif eadid.include?('mc')
23
+ append_to_titleproper(ead, eadid, 'Papers')
24
+ archdesc_level(ead, 'collection')
25
+ end
26
+ end
27
+
28
+ def archdesc_level(ead, content)
29
+ archdesc = ead.xpath('//xmlns:archdesc').first
30
+ archdesc['level'] = content
31
+ end
32
+
33
+ def add_ua_userestrict(ead)
34
+ first_userestrict = ead.xpath('//xmlns:userestrict').first
35
+ userestrict = Nokogiri::XML::Node.new('userestrict', ead)
36
+ first_userestrict.add_next_sibling(userestrict)
37
+ head = Nokogiri::XML::Node.new('head', ead)
38
+ head.content = 'Confidentiality Notice'
39
+ p = Nokogiri::XML::Node.new('p', ead)
40
+ p.content = <<EOF
41
+ This collection may contain materials with sensitive or confidential
42
+ information that is protected under federal or state right to privacy laws and
43
+ regulations. Researchers are advised that the disclosure of certain information
44
+ pertaining to identifiable living individuals represented in this collection
45
+ without the consent of those individuals may have legal ramifications (e.g.,
46
+ a cause of action under common law for invasion of privacy may arise if facts
47
+ concerning an individual's private life are published that would be deemed
48
+ highly offensive to a reasonable person) for which North Carolina State
49
+ University assumes no responsibility.
50
+ EOF
51
+ userestrict.add_child(head)
52
+ userestrict.add_child(p)
53
+ end
54
+
55
+ def append_to_titleproper(ead, eadid, text)
56
+ titleproper = ead.xpath('//xmlns:titleproper').first
57
+ better_titleproper = titleproper.content.strip.chomp + ' ' + text
58
+ titleproper.content = better_titleproper
59
+ num = Nokogiri::XML::Node.new('num', ead)
60
+ better_num = eadid.upcase.gsub('_', '.')
61
+ num.content = better_num
62
+ titleproper.add_child(num)
63
+
64
+ # now also add to archdesc did
65
+ archdesc_did = ead.xpath('//xmlns:archdesc/xmlns:did').first
66
+ unittitle = archdesc_did.xpath('xmlns:unittitle').first
67
+ unittitle.content = better_titleproper
68
+ unitid = archdesc_did.xpath('xmlns:unitid').first
69
+ unitid.content = better_num
70
+ end
71
+
72
+ end
73
+ end
74
+
data/lib/stead/ead.rb ADDED
@@ -0,0 +1,270 @@
1
+ module Stead
2
+ class EadGenerator
3
+ attr_accessor :csv, :ead, :template, :series, :component_parts
4
+
5
+ def initialize(opts = {})
6
+ @csv = opts[:csv] || nil
7
+
8
+ @template = pick_template(opts)
9
+ @eadid = opts[:eadid] if opts[:eadid]
10
+ @base_url = opts[:base_url] if opts[:base_url]
11
+ # component_parts are the rows in the csv file
12
+ @component_parts = csv_to_a
13
+ end
14
+
15
+ def pick_template(opts)
16
+ if opts[:template]
17
+ Nokogiri::XML(File.read(opts[:template]))
18
+ else
19
+ Stead.ead_template_xml
20
+ end
21
+ end
22
+
23
+ def self.from_csv(csv, opts={})
24
+ lines = csv.split(/\r\n|\n/)
25
+ 100.times do
26
+ lines[0] = lines.first.gsub(',,', ',nothing,')
27
+ end
28
+ csv = lines.join("\n")
29
+ self.new(opts.merge(:csv => csv))
30
+ end
31
+
32
+ def eadid_node
33
+ @ead.xpath('//xmlns:eadid').first
34
+ end
35
+
36
+ def add_eadid
37
+ eadid_node.content = @eadid
38
+ end
39
+
40
+ def add_eadid_url
41
+ if @base_url
42
+ eadid_node['url'] = File.join(@base_url, @eadid)
43
+ elsif @url
44
+ eadid_node['url'] = @url
45
+ end
46
+ end
47
+
48
+ def to_ead
49
+ @ead = template.dup
50
+ add_eadid
51
+ add_eadid_url
52
+ @dsc = @ead.xpath('//xmlns:archdesc/xmlns:dsc')[0]
53
+ if series?
54
+ add_series
55
+ end
56
+ @component_parts.each do |cp|
57
+ c = node(file_component_part_name)
58
+ c['level'] = 'file'
59
+ c['audience'] = 'internal' if !cp['internal only'].blank?
60
+ did = node('did')
61
+ c.add_child(did)
62
+ add_did_nodes(cp, did)
63
+ add_containers(cp, did)
64
+ add_scopecontent(cp, did)
65
+ add_accessrestrict(cp, did)
66
+ add_file_component_part(cp, c)
67
+ end
68
+ begin
69
+ valid?
70
+ rescue Stead::InvalidEad
71
+ warn "Invalid EAD"
72
+ ead
73
+ end
74
+ ead
75
+ end
76
+
77
+ def add_series
78
+ add_arrangement
79
+ series = @component_parts.map do |cp|
80
+ [cp['series number'], cp['series title'], cp['series dates']]
81
+ end.uniq
82
+ series.each do |ser|
83
+ add_arrangement_item(ser)
84
+ # create series node and add to dsc
85
+ series_node = node('c01')
86
+ @dsc.add_child(series_node)
87
+ series_node['level'] = 'series'
88
+ # create series did and add to series node
89
+ series_did = node('did')
90
+ series_node.add_child(series_did)
91
+ unitid = node('unitid')
92
+ unitid.content = ser[0]
93
+ unittitle = node('unittitle')
94
+ unittitle.content = ser[1]
95
+ unitdate = node('unitdate')
96
+ unitdate.content = ser[2]
97
+ series_did.add_child(unitid)
98
+ series_did.add_child(unittitle)
99
+ series_did.add_child(unitdate)
100
+ end
101
+ end
102
+
103
+ def add_arrangement
104
+ arrangement = node('arrangement')
105
+ head = node('head')
106
+ head.content = 'Organization of the Collection'
107
+ arrangement.add_child(head)
108
+ p = node('p')
109
+ p.content = 'This collection is organized into series:'
110
+ arrangement.add_child(p)
111
+ list = node('list')
112
+ p.add_child(list)
113
+ @dsc.add_previous_sibling(arrangement)
114
+ end
115
+
116
+ def add_arrangement_item(ser)
117
+ list = @ead.xpath('//xmlns:arrangement/xmlns:p/xmlns:list').first
118
+ item = node('item')
119
+ contents = []
120
+ ser.each do |ser_part|
121
+ contents << ser_part unless ser_part.blank?
122
+ end
123
+ item.content = contents.join(', ')
124
+ list.add_child(item)
125
+ end
126
+
127
+ # metadata is a hash from the @component_part and c is the actual node
128
+ def add_file_component_part(metadata, c)
129
+ if series?
130
+ current_series = find_current_series(metadata)
131
+ current_series.add_child(c)
132
+ else
133
+ @dsc.add_child(c)
134
+ end
135
+ end
136
+
137
+ def find_current_series(cp)
138
+ series_title = cp['series title']
139
+ @ead.xpath("//xmlns:c01/xmlns:did/xmlns:unittitle").each do |node|
140
+ return node.parent.parent if node.content == series_title
141
+ end
142
+ end
143
+
144
+ def file_component_part_name
145
+ if series?
146
+ 'c02'
147
+ else
148
+ 'c01'
149
+ end
150
+ end
151
+
152
+ def add_did_nodes(cp, did)
153
+ field_map.each do |header, element|
154
+ if !cp[header].blank?
155
+ if element.is_a? String
156
+ node = node(element)
157
+ node.content = cp[header]
158
+ did.add_child(node)
159
+ elsif element.is_a? Array
160
+ node1 = node(element[0])
161
+ did.add_child(node1)
162
+ node2 = node(element[1])
163
+ node1.add_child(node2)
164
+ node2.content = cp[header]
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ def add_containers(cp, did)
171
+ ['1', '2', '3'].each do |container_number|
172
+ container_type = cp['container ' + container_number + ' type']
173
+ container_number = cp['container ' + container_number + ' number']
174
+ if !container_type.blank? and !container_number.blank?
175
+ unless valid_container_type?(container_type)
176
+ raise Stead::InvalidContainerType, container_type
177
+ end
178
+ container = node('container')
179
+ container['type'] = container_type
180
+ container['label'] = cp['instance type'] if cp['instance type']
181
+ container.content = container_number
182
+ did.add_child(container)
183
+ end
184
+ end
185
+ end
186
+
187
+ def valid_container_type?(container_type)
188
+ if Stead::CONTAINER_TYPES.include?(container_type)
189
+ return true
190
+ else
191
+ return false
192
+ end
193
+ end
194
+
195
+ def add_scopecontent(cp, did)
196
+ unless cp['scopecontent'].blank?
197
+ scopecontent = node('scopecontent')
198
+ p = node('p')
199
+ p.content = cp['scopecontent']
200
+ scopecontent.add_child(p)
201
+ did.add_next_sibling(scopecontent)
202
+ end
203
+ end
204
+
205
+ def add_accessrestrict(cp, did)
206
+ unless cp['conditions governing access'].blank?
207
+ accessrestrict = node('accessrestrict')
208
+ p = node('p')
209
+ p.content = cp['conditions governing access']
210
+ accessrestrict.add_child(p)
211
+ did.add_next_sibling(accessrestrict)
212
+ end
213
+ end
214
+
215
+ def node(element)
216
+ Nokogiri::XML::Node.new(element, @ead)
217
+ end
218
+
219
+ def field_map
220
+ {'file id' => 'unitid',
221
+ 'file title' => 'unittitle',
222
+ 'file dates' => 'unitdate',
223
+ 'extent' => ['physdesc', 'extent'],
224
+ 'note1' => ['note', 'p'],
225
+ 'note2' => ['note', 'p']
226
+ }
227
+ end
228
+
229
+ def csv_to_a
230
+ a = []
231
+ FasterCSV.parse(csv, :headers => :first_row) do |row|
232
+ a << row.to_hash
233
+ end
234
+ if a.first.keys.include?(nil)
235
+ raise Stead::InvalidCsv
236
+ end
237
+ # TODO invalid if the last row is blank
238
+ # a.sort_by do |row|
239
+ # [
240
+ # row['series number'] || 'z',
241
+ # row['subseries number'] || 'z',
242
+ # row['container 1 number'] || 'z',
243
+ # row['container 2 number'] || 'z',
244
+ # row['file title'] || 'z'
245
+ # ]
246
+ # end
247
+ a
248
+ end
249
+
250
+ def valid?
251
+ unless Stead.xsd.valid?(ead)
252
+ raise Stead::InvalidEad
253
+ end
254
+ end
255
+
256
+ def series?
257
+ if series_found?
258
+ series = true
259
+ end
260
+ end
261
+
262
+ def series_found?
263
+ @component_parts.each do |row|
264
+ return false if row['series number'].blank?
265
+ end
266
+ end
267
+
268
+ end
269
+ end
270
+
@@ -0,0 +1,6 @@
1
+ module Stead
2
+ class InvalidContainerType < RuntimeError; end
3
+ class InvalidEad < RuntimeError; end
4
+ class InvalidCsv < RuntimeError; end
5
+ end
6
+
@@ -0,0 +1,80 @@
1
+ module Stead
2
+
3
+ def self.ead_schema
4
+ File.expand_path(File.join(File.dirname(__FILE__), 'templates','ead.xsd'))
5
+ end
6
+
7
+ def self.xsd
8
+ Nokogiri::XML::Schema(File.read(Stead.ead_schema))
9
+ end
10
+
11
+ def self.ead_template
12
+ File.expand_path(File.join(File.dirname(__FILE__), 'templates','ead.xml'))
13
+ end
14
+
15
+ def self.ead_template_xml
16
+ Nokogiri::XML(File.read(self.ead_template))
17
+ end
18
+
19
+ def self.pretty_write(xml)
20
+ if xml.is_a? String
21
+ self.write(xml)
22
+ elsif xml.is_a? Nokogiri::XML::Document or xml.is_a? Nokogiri::XML::Node
23
+ self.write(xml.to_xml)
24
+ end
25
+ end
26
+
27
+ def self.write(buffer)
28
+
29
+ xsl =<<XSL
30
+ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
31
+ <xsl:output method="xml" encoding="UTF-8"/>
32
+ <xsl:param name="indent-increment" select="' '"/>
33
+ <xsl:template name="newline">
34
+ <xsl:text disable-output-escaping="yes">
35
+ </xsl:text>
36
+ </xsl:template>
37
+ <xsl:template match="comment() | processing-instruction()">
38
+ <xsl:param name="indent" select="''"/>
39
+ <xsl:call-template name="newline"/>
40
+ <xsl:value-of select="$indent"/>
41
+ <xsl:copy />
42
+ </xsl:template>
43
+ <xsl:template match="text()">
44
+ <xsl:param name="indent" select="''"/>
45
+ <xsl:call-template name="newline"/>
46
+ <xsl:value-of select="$indent"/>
47
+ <xsl:value-of select="normalize-space(.)"/>
48
+ </xsl:template>
49
+ <xsl:template match="text()[normalize-space(.)='']"/>
50
+ <xsl:template match="*">
51
+ <xsl:param name="indent" select="''"/>
52
+ <xsl:call-template name="newline"/>
53
+ <xsl:value-of select="$indent"/>
54
+ <xsl:choose>
55
+ <xsl:when test="count(child::*) > 0">
56
+ <xsl:copy>
57
+ <xsl:copy-of select="@*"/>
58
+ <xsl:apply-templates select="*|text()">
59
+ <xsl:with-param name="indent" select="concat ($indent, $indent-increment)"/>
60
+ </xsl:apply-templates>
61
+ <xsl:call-template name="newline"/>
62
+ <xsl:value-of select="$indent"/>
63
+ </xsl:copy>
64
+ </xsl:when>
65
+ <xsl:otherwise>
66
+ <xsl:copy-of select="."/>
67
+ </xsl:otherwise>
68
+ </xsl:choose>
69
+ </xsl:template>
70
+ </xsl:stylesheet>
71
+ XSL
72
+
73
+ doc = Nokogiri::XML(buffer)
74
+ xslt = Nokogiri::XSLT(xsl)
75
+ out = xslt.transform(doc)
76
+ out.to_xml
77
+ end
78
+
79
+ end
80
+