wax 0.9.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +7 -0
- data/lib/cd_demo.rb +16 -0
- data/lib/instance_exec.rb +32 -0
- data/lib/tutorial.rb +190 -0
- data/lib/wax.rb +600 -0
- data/lib/xml_util.rb +108 -0
- data/test/test_wax.rb +569 -0
- data/test/test_xmlutil.rb +86 -0
- metadata +61 -0
data/README.rdoc
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
This package provides a simple API for writing XML documents
|
2
|
+
It is particularly well-suited to writing large XML documents
|
3
|
+
because it doesn't require storing them in a DOM-like structure
|
4
|
+
before outputting them. XML libraries that use that approach can
|
5
|
+
cause the JVM to run out of memory when outputting large XML documents.
|
6
|
+
|
7
|
+
For more information, see http://ociweb.com/wax.
|
data/lib/cd_demo.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'wax'
|
2
|
+
|
3
|
+
url = "http://www.ociweb.com"
|
4
|
+
WAX.write($stdout, "1.0") do
|
5
|
+
comment "This is one of my favorite CDs!"
|
6
|
+
dtd "#{url}/xml/cd.dtd"
|
7
|
+
xslt "cd.xslt"
|
8
|
+
start "cd"
|
9
|
+
attr "year", 2008
|
10
|
+
namespace nil, "#{url}/music", "#{url}/xml/cd.xsd"
|
11
|
+
namespace "date", "#{url}/date", "#{url}/xml/date.xsd"
|
12
|
+
start "artist"
|
13
|
+
attr "name", "Gardot, Melody"
|
14
|
+
child "title", "Worrisome Heart"
|
15
|
+
child "date", "purchaseDate", "4/3/2008"
|
16
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# This solution was found at
|
2
|
+
# http://www.jroller.com/abstractScope/entry/
|
3
|
+
# passing_parameters_to_an_instance
|
4
|
+
|
5
|
+
# Add a bind method to the Proc class.
|
6
|
+
class Proc
|
7
|
+
|
8
|
+
# This creates an UnboundMethod (from a temporary instance method)
|
9
|
+
# and binds it to a given object.
|
10
|
+
# In the case of Object#instance_exec (below), the method is bound to self.
|
11
|
+
def bind(object)
|
12
|
+
block, time = self, Time.now
|
13
|
+
(class << object; self end).class_eval do
|
14
|
+
method_name = "__bind_#{time.to_i}_#{time.usec}"
|
15
|
+
define_method(method_name, &block)
|
16
|
+
method = instance_method(method_name)
|
17
|
+
remove_method(method_name)
|
18
|
+
method
|
19
|
+
end.bind(object)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Add an instance_exec method to the Object class unless it's already there.
|
24
|
+
class Object
|
25
|
+
unless defined? instance_exec # a new method in Ruby 1.9
|
26
|
+
# This binds a block to an arbitrary scope and
|
27
|
+
# calls it (with or without arguments).
|
28
|
+
def instance_exec(*arguments, &block)
|
29
|
+
block.bind(self)[*arguments]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/tutorial.rb
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'wax'
|
2
|
+
|
3
|
+
# This serves as a tutorial for using WAX.
|
4
|
+
|
5
|
+
def out(text)
|
6
|
+
puts "\n\n#{text}\n"
|
7
|
+
end
|
8
|
+
|
9
|
+
# When the no-arg WAX constructor is used, XML is written to stdout.
|
10
|
+
# There are also WAX constructors that take an IO object.
|
11
|
+
wax = WAX.new
|
12
|
+
|
13
|
+
out "Only a root element:"
|
14
|
+
wax.start("car").close
|
15
|
+
# <car/>
|
16
|
+
|
17
|
+
# After a WAX object is closed,
|
18
|
+
# a new one must be created to write more XML.
|
19
|
+
wax = WAX.new
|
20
|
+
|
21
|
+
out "A root element with some text inside:"
|
22
|
+
wax.start("car").text("Prius").close
|
23
|
+
# <car>Prius</car>
|
24
|
+
|
25
|
+
out "Text inside a child element:"
|
26
|
+
wax = WAX.new
|
27
|
+
wax.start("car").start("model").text("Prius").close
|
28
|
+
# <car>
|
29
|
+
# <model>Prius</model>
|
30
|
+
# </car>
|
31
|
+
|
32
|
+
out "The same with the \"child\" convenience method:"
|
33
|
+
wax = WAX.new
|
34
|
+
wax.start("car").child("model", "Prius").close
|
35
|
+
# <car>
|
36
|
+
# <model>Prius</model>
|
37
|
+
# </car>
|
38
|
+
|
39
|
+
out "Text in a CDATA section:"
|
40
|
+
wax = WAX.new
|
41
|
+
wax.start("car").start("model").cdata("1<2>3&4'5\"6").close
|
42
|
+
# <car>
|
43
|
+
# <model>
|
44
|
+
# <![CDATA[1<2>3&4'5\"6]]>
|
45
|
+
# </model>
|
46
|
+
# </car>
|
47
|
+
|
48
|
+
out "Without indentation, on a single line:"
|
49
|
+
wax = WAX.new
|
50
|
+
wax.set_indent(nil)
|
51
|
+
wax.start("car").child("model", "Prius").close
|
52
|
+
# <car><model>Prius</model></car>
|
53
|
+
|
54
|
+
out "Indent with four spaces instead of the default of two:"
|
55
|
+
wax = WAX.new
|
56
|
+
wax.set_indent(" ")
|
57
|
+
wax.start("car").child("model", "Prius").close
|
58
|
+
# <car>
|
59
|
+
# <model>Prius</model>
|
60
|
+
# </car>
|
61
|
+
|
62
|
+
out "Add an attribute:"
|
63
|
+
wax = WAX.new
|
64
|
+
wax.start("car").attr("year", 2008).child("model", "Prius").close
|
65
|
+
# <car year="2008">
|
66
|
+
# <model>Prius</model>
|
67
|
+
# </car>
|
68
|
+
|
69
|
+
out "XML declaration:"
|
70
|
+
wax = WAX.new($stdout, "1.0")
|
71
|
+
wax.start("car").attr("year", 2008).child("model", "Prius").close
|
72
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
73
|
+
# <car year="2008">
|
74
|
+
# <model>Prius</model>
|
75
|
+
# </car>
|
76
|
+
|
77
|
+
out "Comment:"
|
78
|
+
WAX.write do
|
79
|
+
comment "This is a hybrid car."
|
80
|
+
start "car"
|
81
|
+
child "model", "Prius"
|
82
|
+
end
|
83
|
+
# <!-- This is a hybrid car. -->
|
84
|
+
# <car>
|
85
|
+
# <model>Prius</model>
|
86
|
+
# </car>
|
87
|
+
|
88
|
+
out "Processing instruction:"
|
89
|
+
WAX.write do
|
90
|
+
processing_instruction "target", "data"
|
91
|
+
start "car"
|
92
|
+
attr "year", 2008
|
93
|
+
child "model", "Prius"
|
94
|
+
end
|
95
|
+
# <?target data?>
|
96
|
+
# <car year="2008">
|
97
|
+
# <model>Prius</model>
|
98
|
+
# </car>
|
99
|
+
|
100
|
+
out "Associate an XSLT stylesheet:"
|
101
|
+
WAX.write do
|
102
|
+
xslt "car.xslt"
|
103
|
+
start "car"
|
104
|
+
attr "year", 2008
|
105
|
+
child "model", "Prius"
|
106
|
+
end
|
107
|
+
# <?xml-stylesheet type="text/xsl" href="car.xslt"?>
|
108
|
+
# <car year="2008">
|
109
|
+
# <model>Prius</model>
|
110
|
+
# </car>
|
111
|
+
|
112
|
+
out "Associate a default namespace:"
|
113
|
+
WAX.write do
|
114
|
+
xslt "car.xslt"
|
115
|
+
start "car"
|
116
|
+
attr "year", 2008
|
117
|
+
namespace "http://www.ociweb.com/cars"
|
118
|
+
child "model", "Prius"
|
119
|
+
end
|
120
|
+
# <car year="2008"
|
121
|
+
# xmlns="http://www.ociweb.com/cars">
|
122
|
+
# <model>Prius</model>
|
123
|
+
# </car>
|
124
|
+
|
125
|
+
out "Associate a non-default namespace with the XML:"
|
126
|
+
prefix = "c"
|
127
|
+
WAX.write do
|
128
|
+
start prefix, "car"
|
129
|
+
attr "year", 2008
|
130
|
+
namespace prefix, "http://www.ociweb.com/cars"
|
131
|
+
child prefix, "model", "Prius"
|
132
|
+
end
|
133
|
+
# <c:car year="2008"
|
134
|
+
# xmlns:c="http://www.ociweb.com/cars">
|
135
|
+
# <c:model>Prius</c:model>
|
136
|
+
# </c:car>
|
137
|
+
|
138
|
+
out "Associate an XML Schema:"
|
139
|
+
WAX.write do
|
140
|
+
start "car"
|
141
|
+
attr "year", 2008
|
142
|
+
namespace nil, "http://www.ociweb.com/cars", "car.xsd"
|
143
|
+
child "model", "Prius"
|
144
|
+
end
|
145
|
+
# <car year="2008"
|
146
|
+
# xmlns="http://www.ociweb.com/cars"
|
147
|
+
# xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
|
148
|
+
# xsi:schemaLocation="http://www.ociweb.com/cars car.xsd">
|
149
|
+
# <model>Prius</model>
|
150
|
+
# </car>
|
151
|
+
|
152
|
+
out "Associate multiple XML Schemas:"
|
153
|
+
WAX.write do
|
154
|
+
start "car"
|
155
|
+
attr "year", 2008
|
156
|
+
namespace nil, "http://www.ociweb.com/cars", "car.xsd"
|
157
|
+
namespace "m", "http://www.ociweb.com/model", "model.xsd"
|
158
|
+
child "m", "model", "Prius"
|
159
|
+
end
|
160
|
+
# <car year="2008"
|
161
|
+
# xmlns="http://www.ociweb.com/cars"
|
162
|
+
# xmlns:m="http://www.ociweb.com/model"
|
163
|
+
# xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
|
164
|
+
# xsi:schemaLocation="http://www.ociweb.com/cars car.xsd
|
165
|
+
# http://www.ociweb.com/model model.xsd">
|
166
|
+
# <m:model>Prius</m:model>
|
167
|
+
# </car>
|
168
|
+
|
169
|
+
out "Associate a DTD:"
|
170
|
+
WAX.write do
|
171
|
+
dtd "car.dtd"
|
172
|
+
start "car"
|
173
|
+
attr "year", 2008
|
174
|
+
child "model", "Prius"
|
175
|
+
end
|
176
|
+
# <!DOCTYPE car SYSTEM "car.dtd">
|
177
|
+
# <car year="2008">
|
178
|
+
# <model>Prius</model>
|
179
|
+
# </car>
|
180
|
+
|
181
|
+
out "Entity definitions in DOCTYPE:"
|
182
|
+
String url = "http://www.ociweb.com/xml/";
|
183
|
+
WAX.write do
|
184
|
+
entity_def "oci", "Object Computing, Inc."
|
185
|
+
external_entity_def "moreData", url + "moreData.xml"
|
186
|
+
start "root"
|
187
|
+
text "The author works at &oci; in St. Louis, Missouri.",
|
188
|
+
true, false # turning escaping off for entity reference
|
189
|
+
text "&moreData;", true, false
|
190
|
+
end
|
data/lib/wax.rb
ADDED
@@ -0,0 +1,600 @@
|
|
1
|
+
require 'instance_exec'
|
2
|
+
require 'xml_util'
|
3
|
+
|
4
|
+
# This class provides methods that make outputting XML
|
5
|
+
# easy, fast and efficient in terms of memory utilization.
|
6
|
+
#
|
7
|
+
# A WAX object should not be used from multiple threads!
|
8
|
+
#
|
9
|
+
# For more information, see http://www.ociweb.com/wax.
|
10
|
+
#
|
11
|
+
# Copyright 2008 R. Mark Volkmann
|
12
|
+
# This file is part of WAX.
|
13
|
+
#
|
14
|
+
# WAX is free software. You can redistribute it and/or modify it
|
15
|
+
# under the terms of the GNU Lesser General Public License as published
|
16
|
+
# by the Free Software Foundation, either version 3 of the License,
|
17
|
+
# or (at your option) any later version.
|
18
|
+
#
|
19
|
+
# WAX is distributed in the hope that it will be useful,
|
20
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
21
|
+
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
22
|
+
# See the GNU Lesser General Public License for more details.
|
23
|
+
#
|
24
|
+
# You should have received a copy of the GNU Lesser General Public License
|
25
|
+
# along with WAX. If not, see http://www.gnu.org/licenses.
|
26
|
+
#
|
27
|
+
# R. Mark Volkmann, Object Computing, Inc.
|
28
|
+
class WAX
|
29
|
+
|
30
|
+
# The current state of XML output is used to verify that methods
|
31
|
+
# in this class aren't called in an illogical order.
|
32
|
+
# If they are, a RuntimeError is raised.
|
33
|
+
STATES = [:in_prolog, :in_start_tag, :in_element, :after_root]
|
34
|
+
|
35
|
+
# Creates a WAX object, invokes the specified block on it
|
36
|
+
# and calls close on the WAX object.
|
37
|
+
# The writer can be a String file path, an IO object such as a File,
|
38
|
+
# or unspecified to write $stdout.
|
39
|
+
# If the version isn't specified then no XML declaration will be written.
|
40
|
+
def self.write(writer=$stdout, version=nil, &proc)
|
41
|
+
writer = File.new(writer, "w") if writer.kind_of?(String)
|
42
|
+
wax = WAX.new(writer, version)
|
43
|
+
wax.instance_exec(&proc)
|
44
|
+
wax.close
|
45
|
+
end
|
46
|
+
|
47
|
+
# Initializes new instances of this class.
|
48
|
+
def initialize(writer=$stdout, version=nil)
|
49
|
+
@attr_on_new_line = false
|
50
|
+
@check_me = true
|
51
|
+
@close_stream = writer != $stdout
|
52
|
+
@dtd_file_path = nil
|
53
|
+
@entity_defs = []
|
54
|
+
@has_content = @has_indented_content = false
|
55
|
+
@indent = ' '
|
56
|
+
@namespace_uri_to_schema_path_map = {}
|
57
|
+
@parent_stack = []
|
58
|
+
@pending_prefixes = []
|
59
|
+
@prefixes_stack = []
|
60
|
+
@state = :in_prolog
|
61
|
+
@writer = writer
|
62
|
+
write_xml_declaration(version)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Writes an attribute for the currently open element start tag.
|
66
|
+
# If two parameters are specified, they are name and value.
|
67
|
+
# If three parameters are specified, they are prefix, name and value.
|
68
|
+
# If four parameters are specified, they are prefix, name, value and a flag
|
69
|
+
# to indicate whether the attribute should be written on a new line.
|
70
|
+
def attr(p1, p2, p3=nil, p4=nil)
|
71
|
+
if (p3 == nil)
|
72
|
+
prefix, name, value, new_line = nil, p1, p2, false
|
73
|
+
elsif (p4 == nil)
|
74
|
+
prefix, name, value, new_line = p1, p2, p3, false
|
75
|
+
else
|
76
|
+
prefix, name, value, new_line = p1, p2, p3, p4
|
77
|
+
end
|
78
|
+
|
79
|
+
if @check_me
|
80
|
+
bad_state("attr") unless @state == :in_start_tag
|
81
|
+
|
82
|
+
unless prefix == nil
|
83
|
+
XMLUtil.verify_nmtoken(prefix)
|
84
|
+
@pending_prefixes << prefix
|
85
|
+
end
|
86
|
+
|
87
|
+
XMLUtil.verify_nmtoken(name)
|
88
|
+
value = XMLUtil.escape(value)
|
89
|
+
end
|
90
|
+
|
91
|
+
has_prefix = prefix != nil and prefix.length > 0
|
92
|
+
qname = has_prefix ? prefix + ':' + name : name
|
93
|
+
|
94
|
+
if new_line
|
95
|
+
write_indent
|
96
|
+
else
|
97
|
+
write ' '
|
98
|
+
end
|
99
|
+
|
100
|
+
write "#{qname}=\"#{value}\""
|
101
|
+
|
102
|
+
self
|
103
|
+
end
|
104
|
+
|
105
|
+
# Raises a RuntimeError that indicates
|
106
|
+
# the method that was called and the current state that was invalid.
|
107
|
+
def bad_state(method_name)
|
108
|
+
raise "can't call #{method_name} when state is #{@state}"
|
109
|
+
end
|
110
|
+
|
111
|
+
# Writes a blank line to increase readability of the XML.
|
112
|
+
def blank_line
|
113
|
+
nl_text ""
|
114
|
+
end
|
115
|
+
|
116
|
+
# Writes a CDATA section in the content of the current element.
|
117
|
+
def cdata(text)
|
118
|
+
if @check_me
|
119
|
+
bad_state("cdata") if @state == :in_prolog or @state == :after_root
|
120
|
+
end
|
121
|
+
|
122
|
+
text("<![CDATA[" + text + "]]>", true, false)
|
123
|
+
end
|
124
|
+
|
125
|
+
# A convenience method that is a shortcut for
|
126
|
+
# start(prefix, name).text(text).end_element().
|
127
|
+
def child(p1, p2, p3=nil)
|
128
|
+
if (p3 == nil)
|
129
|
+
# only specified element name and text
|
130
|
+
prefix, name, text = nil, p1, p2
|
131
|
+
else
|
132
|
+
# specified element namespace prefix, name and text
|
133
|
+
prefix, name, text = p1, p2, p3
|
134
|
+
end
|
135
|
+
|
136
|
+
bad_state("child") if @check_me and @state == :after_root
|
137
|
+
start(prefix, name).text(text).end_element
|
138
|
+
end
|
139
|
+
|
140
|
+
# Terminates all unterminated elements,
|
141
|
+
# closes the Writer that is being used to output XML,
|
142
|
+
# and insures that nothing else can be written.
|
143
|
+
def close
|
144
|
+
raise "already closed" unless @writer
|
145
|
+
bad_state("close") if @check_me and @state == :in_prolog
|
146
|
+
|
147
|
+
# End all the unended elements.
|
148
|
+
while @parent_stack.size > 0; end_element; end
|
149
|
+
|
150
|
+
if @close_stream
|
151
|
+
@writer.close
|
152
|
+
else
|
153
|
+
@writer.flush
|
154
|
+
end
|
155
|
+
|
156
|
+
@writer = nil
|
157
|
+
end
|
158
|
+
|
159
|
+
# Writes a comment (<!-- text -->).
|
160
|
+
# The comment text cannot contain "--".
|
161
|
+
def comment(text)
|
162
|
+
# Comments can be output in any state.
|
163
|
+
|
164
|
+
XMLUtil.verify_comment(text) if @check_me
|
165
|
+
|
166
|
+
@has_content = @has_indented_content = true
|
167
|
+
terminate_start
|
168
|
+
write_indent if @parent_stack.size > 0
|
169
|
+
|
170
|
+
write "<!-- #{text} -->"
|
171
|
+
write "\n" if will_indent and @parent_stack.size == 0
|
172
|
+
|
173
|
+
self
|
174
|
+
end
|
175
|
+
|
176
|
+
# Writes a DOCTYPE that associates a DTD with the XML document.
|
177
|
+
def dtd(file_path)
|
178
|
+
if @check_me
|
179
|
+
bad_state("dtd") unless @state == :in_prolog
|
180
|
+
XMLUtil.verify_uri(file_path)
|
181
|
+
end
|
182
|
+
|
183
|
+
@dtd_file_path = file_path
|
184
|
+
self
|
185
|
+
end
|
186
|
+
|
187
|
+
# Terminates the current element.
|
188
|
+
# It does so in the shorthand way (/>) if the element has no content,
|
189
|
+
# and in the long way (</name>) if it does.
|
190
|
+
def end_element
|
191
|
+
if @check_me
|
192
|
+
bad_state("end") if @state == :in_prolog or @state == :after_root
|
193
|
+
verify_prefixes
|
194
|
+
end
|
195
|
+
|
196
|
+
write_schema_locations
|
197
|
+
|
198
|
+
name = @parent_stack.pop
|
199
|
+
|
200
|
+
# Namespace prefixes that were in scope for this element
|
201
|
+
# are no longer in scope.
|
202
|
+
@prefixes_stack.pop
|
203
|
+
|
204
|
+
if @has_content
|
205
|
+
write_indent if @has_indented_content
|
206
|
+
write "</#{name}>"
|
207
|
+
else
|
208
|
+
write "/>"
|
209
|
+
end
|
210
|
+
|
211
|
+
@has_content = @has_indented_content = true # new setting for parent
|
212
|
+
|
213
|
+
@state = @parent_stack.size == 0 ? :after_root : :in_element
|
214
|
+
|
215
|
+
self
|
216
|
+
end
|
217
|
+
|
218
|
+
# Adds an entity definition to the internal subset of the DOCTYPE.
|
219
|
+
def entity_def(name, value)
|
220
|
+
bad_state("entity") if @check_me and @state != :in_prolog
|
221
|
+
@entity_defs << "#{name} \"#{value}\""
|
222
|
+
self
|
223
|
+
end
|
224
|
+
|
225
|
+
# Adds an external entity definition to the internal subset of the DOCTYPE.
|
226
|
+
def external_entity_def(name, file_path)
|
227
|
+
entity_def(name + " SYSTEM", file_path)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Gets the indentation characters being used.
|
231
|
+
def get_indent
|
232
|
+
@indent
|
233
|
+
end
|
234
|
+
|
235
|
+
# Determines whether a given namespace prefix is currently in scope.
|
236
|
+
def is_in_scope_prefix(prefix)
|
237
|
+
@prefixes_stack.each do |prefixes|
|
238
|
+
next if prefixes == nil
|
239
|
+
|
240
|
+
# Check for the special case where we are testing for the
|
241
|
+
# default namespace and that's the only namespace in scope.
|
242
|
+
return true if prefix.length == 0 and prefixes.length == 0
|
243
|
+
|
244
|
+
prefixes.split(',').each do |token|
|
245
|
+
return true if token == prefix
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
false
|
250
|
+
end
|
251
|
+
|
252
|
+
# Gets whether "trust me" mode is enabled.
|
253
|
+
def is_trust_me
|
254
|
+
!@check_me
|
255
|
+
end
|
256
|
+
|
257
|
+
# Writes a namespace declaration in the start tag of the current element.
|
258
|
+
# If one parameter is specified, it is the default namespace uri.
|
259
|
+
# If two parameters are specified, they are prefix and uri.
|
260
|
+
# If three parameters are specified, they are prefix, uri and schema location.
|
261
|
+
def namespace(p1, p2=nil, p3=nil)
|
262
|
+
if (p2 == nil)
|
263
|
+
# only specified the default namespace uri
|
264
|
+
prefix, uri, schema_path = nil, p1, nil
|
265
|
+
elsif (p3 == nil)
|
266
|
+
# only specified a namespace prefix and uri
|
267
|
+
prefix, uri, schema_path = p1, p2, nil
|
268
|
+
else
|
269
|
+
# specified namespace prefix, uri and schema location
|
270
|
+
prefix, uri, schema_path = p1, p2, p3
|
271
|
+
end
|
272
|
+
|
273
|
+
prefix = "" if prefix == nil
|
274
|
+
has_prefix = prefix.length > 0
|
275
|
+
|
276
|
+
if @check_me
|
277
|
+
bad_state("namespace") unless @state == :in_start_tag
|
278
|
+
|
279
|
+
XMLUtil.verify_nmtoken(prefix) if has_prefix
|
280
|
+
XMLUtil.verify_uri(uri)
|
281
|
+
XMLUtil.verify_uri(schema_path) unless schema_path == nil
|
282
|
+
end
|
283
|
+
|
284
|
+
# Verify that the prefix isn't already defined in the current scope.
|
285
|
+
if is_in_scope_prefix(prefix)
|
286
|
+
raise ArgumentError,
|
287
|
+
"The namespace prefix \"#{prefix}\" is already in scope."
|
288
|
+
end
|
289
|
+
|
290
|
+
if will_indent
|
291
|
+
write_indent
|
292
|
+
else
|
293
|
+
write ' '
|
294
|
+
end
|
295
|
+
|
296
|
+
write "xmlns"
|
297
|
+
write(':' + prefix) if has_prefix
|
298
|
+
write "=\"#{uri}\""
|
299
|
+
|
300
|
+
if schema_path != nil
|
301
|
+
@namespace_uri_to_schema_path_map[uri] = schema_path
|
302
|
+
end
|
303
|
+
|
304
|
+
# Add this prefix to the list of those in scope for this element.
|
305
|
+
prefixes = @prefixes_stack.pop
|
306
|
+
if prefixes == nil
|
307
|
+
prefixes = prefix
|
308
|
+
else
|
309
|
+
prefixes << ',' + prefix
|
310
|
+
end
|
311
|
+
@prefixes_stack.push(prefixes)
|
312
|
+
|
313
|
+
@attr_on_new_line = true # for the next attribute
|
314
|
+
|
315
|
+
self
|
316
|
+
end
|
317
|
+
|
318
|
+
# Writes text preceded by a newline.
|
319
|
+
def nl_text(text)
|
320
|
+
text text, true, @check_me
|
321
|
+
end
|
322
|
+
|
323
|
+
# Writes a processing instruction.
|
324
|
+
def processing_instruction(target, data)
|
325
|
+
if @check_me
|
326
|
+
bad_state("pi") if @state == :after_root
|
327
|
+
XMLUtil.verify_nmtoken(target)
|
328
|
+
end
|
329
|
+
|
330
|
+
@has_content = @has_indented_content = true
|
331
|
+
terminate_start
|
332
|
+
write_indent if @parent_stack.size > 0
|
333
|
+
|
334
|
+
write "<?#{target} #{data}?>"
|
335
|
+
write("\n") if will_indent and @parent_stack.size == 0
|
336
|
+
|
337
|
+
self
|
338
|
+
end
|
339
|
+
|
340
|
+
# Sets the indentation characters to use.
|
341
|
+
# The only valid values are
|
342
|
+
# a single tab, one or more spaces, an empty string, or null.
|
343
|
+
# Passing "" causes elements to be output on separate lines,
|
344
|
+
# but not indented.
|
345
|
+
# Passing null causes all output to be on a single line.
|
346
|
+
def set_indent(indent)
|
347
|
+
if indent == nil
|
348
|
+
@indent = indent
|
349
|
+
|
350
|
+
elsif indent.kind_of?(Fixnum)
|
351
|
+
count, @indent = indent, ''
|
352
|
+
|
353
|
+
if count < 0
|
354
|
+
raise ArgumentError, "can't indent a negative number of spaces"
|
355
|
+
end
|
356
|
+
|
357
|
+
if count > 4
|
358
|
+
raise ArgumentError, "#{count} is an unreasonable indentation"
|
359
|
+
end
|
360
|
+
|
361
|
+
for i in 1..count; @indent << ' '; end
|
362
|
+
|
363
|
+
return
|
364
|
+
|
365
|
+
elsif indent.kind_of?(String)
|
366
|
+
# Note that the parens on the next line are necessary
|
367
|
+
# because the assignment operator has higher precedence than "or".
|
368
|
+
valid = (indent == nil or indent.length == 0 or indent == "\t")
|
369
|
+
|
370
|
+
unless valid
|
371
|
+
# It can only be valid now if every character is a space.
|
372
|
+
valid = true
|
373
|
+
for i in 0...indent.length
|
374
|
+
unless indent[i] == 32 # space
|
375
|
+
valid = false
|
376
|
+
break
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
raise ArgumentError, "invalid indent value #{indent}" unless valid
|
382
|
+
|
383
|
+
@indent = indent
|
384
|
+
|
385
|
+
else
|
386
|
+
raise ArgumentError, "invalid indent value #{indent}"
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
# Gets whether "trust me" mode is enabled.
|
391
|
+
# When disabled (the default),
|
392
|
+
# proper order of method calls is verified,
|
393
|
+
# method parameter values are verified,
|
394
|
+
# element and attribute names are verified to be NMTokens,
|
395
|
+
# and reserved characters in element/attribute text
|
396
|
+
# are replaced by built-in entity references.
|
397
|
+
# The main reason to enable "trust me" mode is for performance
|
398
|
+
# which is typically good even when disabled.
|
399
|
+
def set_trust_me(trust_me)
|
400
|
+
@check_me = !trust_me
|
401
|
+
end
|
402
|
+
|
403
|
+
# Writes the start tag for a given element name, but doesn't terminate it.
|
404
|
+
# If one parameter is specified, it is the element name.
|
405
|
+
# If two parameters are specified, they are prefix and name.
|
406
|
+
def start(p1, p2=nil)
|
407
|
+
if (p2 == nil)
|
408
|
+
# only specified element name
|
409
|
+
prefix, name = nil, p1
|
410
|
+
else
|
411
|
+
# specified element namespace prefix, name and text
|
412
|
+
prefix, name = p1, p2
|
413
|
+
end
|
414
|
+
|
415
|
+
@has_content = @has_indented_content = true
|
416
|
+
terminate_start
|
417
|
+
@has_content = false
|
418
|
+
|
419
|
+
if @check_me
|
420
|
+
bad_state("start") if @state == :after_root
|
421
|
+
if prefix != nil
|
422
|
+
XMLUtil.verify_nmtoken(prefix)
|
423
|
+
@pending_prefixes << prefix
|
424
|
+
end
|
425
|
+
XMLUtil.verify_nmtoken(name)
|
426
|
+
end
|
427
|
+
|
428
|
+
# If this is the root element ...
|
429
|
+
write_doctype(name) if @state == :in_prolog
|
430
|
+
|
431
|
+
# Can't add to pendingPrefixes until
|
432
|
+
# previous start tag has been terminated.
|
433
|
+
@pending_prefixes << prefix if @check_me and prefix != nil
|
434
|
+
|
435
|
+
write_indent if @parent_stack.size > 0
|
436
|
+
|
437
|
+
has_prefix = prefix != nil and prefix.length > 0
|
438
|
+
qname = has_prefix ? prefix + ':' + name : name
|
439
|
+
|
440
|
+
write '<' + qname
|
441
|
+
|
442
|
+
@parent_stack.push(qname)
|
443
|
+
|
444
|
+
# No namespace prefixes have been associated with this element yet.
|
445
|
+
@prefixes_stack.push(nil)
|
446
|
+
|
447
|
+
@state = :in_start_tag
|
448
|
+
|
449
|
+
self
|
450
|
+
end
|
451
|
+
|
452
|
+
# Closes the start tag, with > or />, that had been kept open
|
453
|
+
# waiting for more namespace declarations and attributes.
|
454
|
+
def terminate_start
|
455
|
+
verify_prefixes if @check_me
|
456
|
+
return if @state != :in_start_tag
|
457
|
+
write_schema_locations
|
458
|
+
write '>'
|
459
|
+
@attr_on_new_line = false # reset
|
460
|
+
@state = :in_element
|
461
|
+
end
|
462
|
+
|
463
|
+
# Writes text inside the content of the current element.
|
464
|
+
def text(text, newline=false, escape=@check_me)
|
465
|
+
if @check_me
|
466
|
+
bad_state("text") if @state == :in_prolog or @state == :after_root
|
467
|
+
end
|
468
|
+
|
469
|
+
@has_content = true
|
470
|
+
@has_indented_content = newline
|
471
|
+
terminate_start
|
472
|
+
|
473
|
+
if text != nil and text.length > 0
|
474
|
+
write_indent if newline
|
475
|
+
text = XMLUtil.escape(text) if escape
|
476
|
+
write text
|
477
|
+
elsif newline
|
478
|
+
write "\n"
|
479
|
+
end
|
480
|
+
|
481
|
+
self
|
482
|
+
end
|
483
|
+
|
484
|
+
# Verifies that all the pending namespace prefix are currently in scope.
|
485
|
+
# ArgumentError is raises if any aren't in scope.
|
486
|
+
def verify_prefixes
|
487
|
+
@pending_prefixes.each do |prefix|
|
488
|
+
if !is_in_scope_prefix(prefix)
|
489
|
+
raise ArgumentError,
|
490
|
+
"The namespace prefix \"#{prefix}\" isn't in scope."
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
@pending_prefixes.clear
|
495
|
+
end
|
496
|
+
|
497
|
+
# Determines whether XML should be indented.
|
498
|
+
def will_indent
|
499
|
+
@indent != nil
|
500
|
+
end
|
501
|
+
|
502
|
+
# Writes the to_s value of an Object to the writer.
|
503
|
+
def write(data)
|
504
|
+
raise "attempting to write XML after close has been called" unless @writer
|
505
|
+
@writer.write(data.to_s)
|
506
|
+
end
|
507
|
+
|
508
|
+
# Writes a DOCTYPE.
|
509
|
+
def write_doctype(root_element_name)
|
510
|
+
return if @dtd_file_path == nil and @entity_defs.empty?
|
511
|
+
|
512
|
+
write "<!DOCTYPE #{root_element_name}"
|
513
|
+
write " SYSTEM \"#{@dtd_file_path}\"" unless @dtd_file_path == nil
|
514
|
+
|
515
|
+
if not @entity_defs.empty?
|
516
|
+
write " ["
|
517
|
+
|
518
|
+
@entity_defs.each do |entity_def|
|
519
|
+
write "\n" + @indent if will_indent
|
520
|
+
write "<!ENTITY #{entity_def}>"
|
521
|
+
end
|
522
|
+
|
523
|
+
write "\n" if will_indent
|
524
|
+
write ']'
|
525
|
+
|
526
|
+
@entity_defs.clear
|
527
|
+
end
|
528
|
+
|
529
|
+
write '>'
|
530
|
+
write "\n" if will_indent
|
531
|
+
end
|
532
|
+
|
533
|
+
# Writes the proper amount of indentation
|
534
|
+
# given the current nesting of elements.
|
535
|
+
def write_indent
|
536
|
+
return unless will_indent
|
537
|
+
write "\n"
|
538
|
+
for i in 0...@parent_stack.size
|
539
|
+
write @indent
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
# Writes the namespace declaration for the XMLSchema-instance namespace
|
544
|
+
# and writes the schemaLocation attribute
|
545
|
+
# which associates namespace URIs with schema locations.
|
546
|
+
def write_schema_locations
|
547
|
+
return if @namespace_uri_to_schema_path_map.empty?
|
548
|
+
|
549
|
+
# Write the attributes needed to associate XML Schemas with this XML.
|
550
|
+
schema_location = ""
|
551
|
+
@namespace_uri_to_schema_path_map.each_pair do |uri, path|
|
552
|
+
if schema_location.length > 0 # not first pair output
|
553
|
+
if will_indent
|
554
|
+
schema_location << "\n"
|
555
|
+
for i in 0..@parent_stack.size
|
556
|
+
schema_location << @indent
|
557
|
+
end
|
558
|
+
else
|
559
|
+
schema_location << ' '
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
schema_location << uri + ' ' + path
|
564
|
+
end
|
565
|
+
|
566
|
+
namespace("xsi", XMLUtil::XMLSCHEMA_INSTANCE_NS)
|
567
|
+
attr("xsi", "schemaLocation", schema_location, will_indent)
|
568
|
+
@attr_on_new_line = true # for the next attribute
|
569
|
+
@namespace_uri_to_schema_path_map.clear
|
570
|
+
end
|
571
|
+
|
572
|
+
# Writes an XML declaration.
|
573
|
+
# Note that regardless of indentation,
|
574
|
+
# a newline is always written after this.
|
575
|
+
def write_xml_declaration(version)
|
576
|
+
return if version == nil
|
577
|
+
XMLUtil.verify_version(version) if @check_me
|
578
|
+
|
579
|
+
write "<?xml version=\"#{version}\" " +
|
580
|
+
"encoding=\"#{XMLUtil::DEFAULT_ENCODING}\"?>\n"
|
581
|
+
end
|
582
|
+
|
583
|
+
# Writes an "xml-stylesheet" processing instruction.
|
584
|
+
def xslt(file_path)
|
585
|
+
if @check_me
|
586
|
+
bad_state("xslt") unless @state == :in_prolog
|
587
|
+
XMLUtil.verify_uri(file_path)
|
588
|
+
end
|
589
|
+
|
590
|
+
@state = :in_prolog
|
591
|
+
|
592
|
+
processing_instruction("xml-stylesheet",
|
593
|
+
"type=\"text/xsl\" href=\"#{file_path}\"")
|
594
|
+
end
|
595
|
+
|
596
|
+
private :bad_state, :is_in_scope_prefix, :terminate_start,
|
597
|
+
:verify_prefixes, :will_indent, :write_doctype, :write_indent,
|
598
|
+
:write_schema_locations, :write_xml_declaration
|
599
|
+
|
600
|
+
end
|