wax 0.9.4
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/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
|