Class: WAX

Inherits:
Object show all
Defined in:
lib/wax.rb

Overview

This class provides methods that make outputting XML easy, fast and efficient in terms of memory utilization.

A WAX object should not be used from multiple threads!

For more information, see www.ociweb.com/wax.

Copyright 2008 R. Mark Volkmann This file is part of WAX.

WAX is free software. You can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

WAX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with WAX. If not, see www.gnu.org/licenses.

  1. Mark Volkmann, Object Computing, Inc.

Constant Summary collapse

STATES =

The current state of XML output is used to verify that methods in this class aren’t called in an illogical order. If they are, a RuntimeError is raised.

[:in_prolog, :in_start_tag, :in_element, :after_root]

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(writer = $stdout, version = nil) ⇒ WAX

Initializes new instances of this class.



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/wax.rb', line 48

def initialize(writer=$stdout, version=nil)
  @attr_on_new_line = false
  @check_me = true
  @close_stream = writer != $stdout
  @dtd_file_path = nil
  @entity_defs = []
  @has_content = @has_indented_content = false
  @indent = '  '
  @namespace_uri_to_schema_path_map = {}
  @parent_stack = []
  @pending_prefixes = []
  @prefixes_stack = []
  @state = :in_prolog
  @writer = writer 
  write_xml_declaration(version)
end

Class Method Details

.write(writer = $stdout, version = nil, &proc) ⇒ Object

Creates a WAX object, invokes the specified block on it and calls close on the WAX object. The writer can be a String file path, an IO object such as a File, or unspecified to write $stdout. If the version isn’t specified then no XML declaration will be written.



40
41
42
43
44
45
# File 'lib/wax.rb', line 40

def self.write(writer=$stdout, version=nil, &proc)
  writer = File.new(writer, "w") if writer.kind_of?(String)
  wax = WAX.new(writer, version)
  wax.instance_exec(&proc)
  wax.close
end

Instance Method Details

#attr(p1, p2, p3 = nil, p4 = nil) ⇒ Object

Writes an attribute for the currently open element start tag. If two parameters are specified, they are name and value. If three parameters are specified, they are prefix, name and value. If four parameters are specified, they are prefix, name, value and a flag to indicate whether the attribute should be written on a new line.



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/wax.rb', line 70

def attr(p1, p2, p3=nil, p4=nil)
  if (p3 == nil)
    prefix, name, value, new_line = nil, p1, p2, false
  elsif (p4 == nil)
    prefix, name, value, new_line = p1, p2, p3, false
  else
    prefix, name, value, new_line = p1, p2, p3, p4
  end

  if @check_me
    bad_state("attr") unless @state == :in_start_tag

    unless prefix == nil
      XMLUtil.verify_nmtoken(prefix)
      @pending_prefixes << prefix
    end

     XMLUtil.verify_nmtoken(name)
     value = XMLUtil.escape(value)
  end

  has_prefix = prefix != nil and prefix.length > 0
  qname = has_prefix ? prefix + ':' + name : name

  if new_line
    write_indent
  else
    write ' '
  end
      
  write "#{qname}=\"#{value}\""

  self
end

#blank_lineObject

Writes a blank line to increase readability of the XML.



112
113
114
# File 'lib/wax.rb', line 112

def blank_line
  nl_text ""
end

#cdata(text) ⇒ Object

Writes a CDATA section in the content of the current element.



117
118
119
120
121
122
123
# File 'lib/wax.rb', line 117

def cdata(text)
  if @check_me
    bad_state("cdata") if @state == :in_prolog or @state == :after_root
  end

  text("<![CDATA[" + text + "]]>", true, false)
end

#child(p1, p2, p3 = nil) ⇒ Object

A convenience method that is a shortcut for start(prefix, name).text(text).end_element().



127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/wax.rb', line 127

def child(p1, p2, p3=nil)
  if (p3 == nil)
    # only specified element name and text
    prefix, name, text = nil, p1, p2
  else
    # specified element namespace prefix, name and text
    prefix, name, text = p1, p2, p3
  end

  bad_state("child") if @check_me and @state == :after_root
  start(prefix, name).text(text).end_element
end

#closeObject

Terminates all unterminated elements, closes the Writer that is being used to output XML, and insures that nothing else can be written.



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/wax.rb', line 143

def close
  raise "already closed" unless @writer
  bad_state("close") if @check_me and @state == :in_prolog

  # End all the unended elements.
  while @parent_stack.size > 0; end_element; end

  if @close_stream
    @writer.close
  else
    @writer.flush
  end

  @writer = nil
end

#comment(text) ⇒ Object

Writes a comment (&lt;!– text –&gt;). The comment text cannot contain “–”.



161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/wax.rb', line 161

def comment(text)
  # Comments can be output in any state.

  XMLUtil.verify_comment(text) if @check_me
      
  @has_content = @has_indented_content = true
  terminate_start
  write_indent if @parent_stack.size > 0

  write "<!-- #{text} -->"
  write "\n" if will_indent and @parent_stack.size == 0

  self
end

#dtd(file_path) ⇒ Object

Writes a DOCTYPE that associates a DTD with the XML document.



177
178
179
180
181
182
183
184
185
# File 'lib/wax.rb', line 177

def dtd(file_path)
  if @check_me
    bad_state("dtd") unless @state == :in_prolog
    XMLUtil.verify_uri(file_path)
  end

  @dtd_file_path = file_path
  self
end

#end_elementObject

Terminates the current element. It does so in the shorthand way (/&gt;) if the element has no content, and in the long way (&lt;/name&gt;) if it does.



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/wax.rb', line 190

def end_element
  if @check_me
    bad_state("end") if @state == :in_prolog or @state == :after_root
    verify_prefixes
  end

  write_schema_locations

  name = @parent_stack.pop

  # Namespace prefixes that were in scope for this element
  # are no longer in scope.
  @prefixes_stack.pop

  if @has_content
    write_indent if @has_indented_content
    write "</#{name}>"
  else
    write "/>"
  end

  @has_content = @has_indented_content = true # new setting for parent

  @state = @parent_stack.size == 0 ? :after_root : :in_element

  self
end

#entity_def(name, value) ⇒ Object

Adds an entity definition to the internal subset of the DOCTYPE.



219
220
221
222
223
# File 'lib/wax.rb', line 219

def entity_def(name, value)
  bad_state("entity") if @check_me and @state != :in_prolog
  @entity_defs << "#{name} \"#{value}\""
  self
end

#external_entity_def(name, file_path) ⇒ Object

Adds an external entity definition to the internal subset of the DOCTYPE.



226
227
228
# File 'lib/wax.rb', line 226

def external_entity_def(name, file_path)
  entity_def(name + " SYSTEM", file_path)
end

#get_indentObject

Gets the indentation characters being used.



231
232
233
# File 'lib/wax.rb', line 231

def get_indent
  @indent
end

#is_trust_meObject

Gets whether “trust me” mode is enabled.



253
254
255
# File 'lib/wax.rb', line 253

def is_trust_me
  !@check_me
end

#namespace(p1, p2 = nil, p3 = nil) ⇒ Object

Writes a namespace declaration in the start tag of the current element. If one parameter is specified, it is the default namespace uri. If two parameters are specified, they are prefix and uri. If three parameters are specified, they are prefix, uri and schema location.



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/wax.rb', line 261

def namespace(p1, p2=nil, p3=nil)
  if (p2 == nil)
    # only specified the default namespace uri
    prefix, uri, schema_path = nil, p1, nil
  elsif (p3 == nil)
    # only specified a namespace prefix and uri
    prefix, uri, schema_path = p1, p2, nil
  else
    # specified namespace prefix, uri and schema location
    prefix, uri, schema_path = p1, p2, p3
  end

  prefix = "" if prefix == nil
  has_prefix = prefix.length > 0

  if @check_me
    bad_state("namespace") unless @state == :in_start_tag

    XMLUtil.verify_nmtoken(prefix) if has_prefix
    XMLUtil.verify_uri(uri)
    XMLUtil.verify_uri(schema_path) unless schema_path == nil
  end

  # Verify that the prefix isn't already defined in the current scope.
  if is_in_scope_prefix(prefix)
    raise ArgumentError,
      "The namespace prefix \"#{prefix}\" is already in scope."
  end

  if will_indent
    write_indent
  else
    write ' '
  end
      
  write "xmlns"
  write(':' + prefix) if has_prefix
  write "=\"#{uri}\""
      
  if schema_path != nil
    @namespace_uri_to_schema_path_map[uri] = schema_path
  end

  # Add this prefix to the list of those in scope for this element.
  prefixes = @prefixes_stack.pop
  if prefixes == nil
    prefixes = prefix
  else
    prefixes << ',' + prefix
  end
  @prefixes_stack.push(prefixes)

  @attr_on_new_line = true # for the next attribute

  self
end

#nl_text(text) ⇒ Object

Writes text preceded by a newline.



319
320
321
# File 'lib/wax.rb', line 319

def nl_text(text)
  text text, true, @check_me
end

#processing_instruction(target, data) ⇒ Object

Writes a processing instruction.



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/wax.rb', line 324

def processing_instruction(target, data)
  if @check_me
    bad_state("pi") if @state == :after_root
    XMLUtil.verify_nmtoken(target)
  end
      
  @has_content = @has_indented_content = true
  terminate_start
  write_indent if @parent_stack.size > 0

  write "<?#{target} #{data}?>"
  write("\n") if will_indent and @parent_stack.size == 0

  self
end

#set_indent(indent) ⇒ Object

Sets the indentation characters to use. The only valid values are a single tab, one or more spaces, an empty string, or null. Passing “” causes elements to be output on separate lines, but not indented. Passing null causes all output to be on a single line.



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/wax.rb', line 346

def set_indent(indent)
  if indent == nil
    @indent = indent

  elsif indent.kind_of?(Fixnum)
    count, @indent = indent, ''

    if count < 0
      raise ArgumentError, "can't indent a negative number of spaces"
    end

    if count > 4
      raise ArgumentError, "#{count} is an unreasonable indentation"
    end

    for i in 1..count; @indent << ' '; end

    return

  elsif indent.kind_of?(String)
    # Note that the parens on the next line are necessary
    # because the assignment operator has higher precedence than "or".
    valid = (indent == nil or indent.length == 0 or indent == "\t")
    
    unless valid
      # It can only be valid now if every character is a space.
      valid = true
      for i in 0...indent.length
        unless indent[i] == 32 # space
          valid = false
          break
        end
      end
    end
    
    raise ArgumentError, "invalid indent value #{indent}" unless valid
    
    @indent = indent

  else
    raise ArgumentError, "invalid indent value #{indent}"
  end
end

#set_trust_me(trust_me) ⇒ Object

Gets whether “trust me” mode is enabled. When disabled (the default), proper order of method calls is verified, method parameter values are verified, element and attribute names are verified to be NMTokens, and reserved characters in element/attribute text are replaced by built-in entity references. The main reason to enable “trust me” mode is for performance which is typically good even when disabled.



399
400
401
# File 'lib/wax.rb', line 399

def set_trust_me(trust_me)
  @check_me = !trust_me
end

#start(p1, p2 = nil) ⇒ Object

Writes the start tag for a given element name, but doesn’t terminate it. If one parameter is specified, it is the element name. If two parameters are specified, they are prefix and name.



406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'lib/wax.rb', line 406

def start(p1, p2=nil)
  if (p2 == nil)
    # only specified element name
    prefix, name = nil, p1
  else
    # specified element namespace prefix, name and text
    prefix, name = p1, p2
  end

  @has_content = @has_indented_content = true
  terminate_start
  @has_content = false

  if @check_me
    bad_state("start") if @state == :after_root
    if prefix != nil
      XMLUtil.verify_nmtoken(prefix)
      @pending_prefixes << prefix
    end
    XMLUtil.verify_nmtoken(name)
  end

  # If this is the root element ...
  write_doctype(name) if @state == :in_prolog

  # Can't add to pendingPrefixes until
  # previous start tag has been terminated.
  @pending_prefixes << prefix if @check_me and prefix != nil

  write_indent if @parent_stack.size > 0

  has_prefix = prefix != nil and prefix.length > 0
  qname = has_prefix ? prefix + ':' + name : name

  write '<' + qname

  @parent_stack.push(qname)

  # No namespace prefixes have been associated with this element yet.
  @prefixes_stack.push(nil)

  @state = :in_start_tag

  self
end

#text(text, newline = false, escape = @check_me) ⇒ Object

Writes text inside the content of the current element.



464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# File 'lib/wax.rb', line 464

def text(text, newline=false, escape=@check_me)
  if @check_me
    bad_state("text") if @state == :in_prolog or @state == :after_root
  end

  @has_content = true
  @has_indented_content = newline
  terminate_start

  if text != nil and text.length > 0
    write_indent if newline
    text = XMLUtil.escape(text) if escape
    write text
  elsif newline
    write "\n"
  end

  self
end

#write(data) ⇒ Object

Writes the to_s value of an Object to the writer.



503
504
505
506
# File 'lib/wax.rb', line 503

def write(data)
  raise "attempting to write XML after close has been called" unless @writer
  @writer.write(data.to_s)
end

#xslt(file_path) ⇒ Object

Writes an “xml-stylesheet” processing instruction.



584
585
586
587
588
589
590
591
592
593
594
# File 'lib/wax.rb', line 584

def xslt(file_path)
  if @check_me
    bad_state("xslt") unless @state == :in_prolog
    XMLUtil.verify_uri(file_path)
  end

  @state = :in_prolog

  processing_instruction("xml-stylesheet",
    "type=\"text/xsl\" href=\"#{file_path}\"")
end