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]
NONWINDOWS_CR =
"\n"
WINDOWS_CR =
"\r\n"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

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

Initializes new instances of this class.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/wax.rb', line 58

def initialize(writer=$stdout, version=nil)
  @attr_on_new_line = false
  @check_me = true
  @close_stream = writer != $stdout
  @dtd_specified = false
  @in_commented_start = false
  @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 
  @xslt_specified = false
  use_non_windows_cr
  write_xml_declaration(version)
end

Instance Attribute Details

#stateObject (readonly)

Returns the value of attribute state.



38
39
40
# File 'lib/wax.rb', line 38

def state
  @state
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 out $stdout. If the version isn’t specified then no XML declaration will be written.



45
46
47
48
49
50
51
# File 'lib/wax.rb', line 45

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
  wax # return instance so caller can get configuration info. like @cr
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.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/wax.rb', line 84

def attr(p1, p2, p3=nil, p4=nil)
  if (p3 == nil)
    prefix, name, value, new_line = nil, p1, p2, @attr_on_new_line
  elsif (p4 == nil)
    prefix, name, value, new_line = p1, p2, p3, @attr_on_new_line
  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_name(prefix)
      @pending_prefixes << prefix
    end

     XMLUtil.verify_name(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
    out ' '
  end
      
  out "#{qname}=\"#{value}\""

  self
end

#blank_lineObject

Writes a blank line to increase readability of the XML.



126
127
128
# File 'lib/wax.rb', line 126

def blank_line
  nl_text ""
end

#cdata(text) ⇒ Object

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



131
132
133
134
135
136
137
# File 'lib/wax.rb', line 131

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!().



141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/wax.rb', line 141

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!
end

#closeObject

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



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/wax.rb', line 157

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!; 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 “–”.



175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/wax.rb', line 175

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

  out "<!-- #{text} -->"
  out "#{@cr}" if will_indent and @parent_stack.size == 0

  self
end

#commented_start(p1, p2 = nil) ⇒ Object

Writes a commented start tag for a given element name, but doesn’t terminate it.



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/wax.rb', line 192

def commented_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

  @in_commented_start = true
  start(prefix, name)
  @in_commented_start = false

  self
end

#dtd(file_path) ⇒ Object

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



209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/wax.rb', line 209

def dtd(file_path)
  raise "can't specify more than one DTD" if @dtd_specified

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

  @dtd_file_path = file_path
  @dtd_specified = true
  self
end

#end!Object

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. The name of this method ends in ! because end is a keyword in Ruby.



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/wax.rb', line 226

def end!
  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

  # Check for hypen at beginning of element name
  # which indicates that the commentedStart method was used.
  was_commented_start = name[0] == '-'[0]

  if @has_content
    write_indent if @has_indented_content
    out '</'
    out was_commented_start ? name[1..-1] : name
    out was_commented_start ? '-->' : '>'
  else
    out was_commented_start ? '/-->' : '/>'
  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.



261
262
263
264
265
# File 'lib/wax.rb', line 261

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.



268
269
270
# File 'lib/wax.rb', line 268

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

#get_crObject

Gets the carriage return characters currently being used.



273
274
275
# File 'lib/wax.rb', line 273

def get_cr
  @cr
end

#get_indentObject

Gets the indentation characters being used.



278
279
280
# File 'lib/wax.rb', line 278

def get_indent
  @indent
end

#is_trust_meObject

Gets whether “trust me” mode is enabled.



300
301
302
# File 'lib/wax.rb', line 300

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.



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/wax.rb', line 308

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_name(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
    out ' '
  end
      
  out "xmlns"
  out(':' + prefix) if has_prefix
  out "=\"#{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.



366
367
368
# File 'lib/wax.rb', line 366

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

#no_indents_or_crsObject

Don’t output indent output or write carriage returns. Write out the XML on a single line.



372
373
374
# File 'lib/wax.rb', line 372

def no_indents_or_crs
  set_indent nil
end

#out(data) ⇒ Object

Writes the to_s value of an Object to the writer.



377
378
379
380
# File 'lib/wax.rb', line 377

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

#processing_instruction(target, data) ⇒ Object

Writes a processing instruction.



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/wax.rb', line 383

def processing_instruction(target, data)
  if @check_me
    bad_state("pi") if @state == :after_root

    # Special handling for this processing instruction
    # since starting with "xml" is reserved.
    XMLUtil.verify_name(target) unless target == "xml-stylesheet"
  end
      
  @has_content = @has_indented_content = true
  terminate_start
  write_indent if @parent_stack.size > 0

  out "<?#{target} #{data}?>"
  out("#{@cr}") 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 nil. Passing “” causes elements to be output on separate lines, but not indented. Passing nil causes all output to be on a single line.



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
451
452
453
454
# File 'lib/wax.rb', line 408

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 @check_me && count > 4
      raise ArgumentError, "#{count} is an unreasonable indentation"
    end

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

    return

  elsif indent.kind_of?(String)
    if @check_me
      # 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
      
      if !valid or (indent != nil and indent.length > 4)
        raise ArgumentError, "invalid indent value #{indent}"
      end
    end
    
    @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.



465
466
467
# File 'lib/wax.rb', line 465

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.



472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
# File 'lib/wax.rb', line 472

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_name(prefix)
      @pending_prefixes << prefix
    end
    XMLUtil.verify_name(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

  if (@in_commented_start)
      out '<!--' + qname
      # Add a "marker" to the element name on the stack
      # so the end method knows to terminate the comment.
      @parent_stack.push('-' + qname)
  else
    out '<' + qname
    @parent_stack.push(qname)
  end

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

  @state = :in_start_tag

  self
end

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

Writes text inside the content of the current element.



536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
# File 'lib/wax.rb', line 536

def text(obj, 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

  text = obj.to_s if obj

  if obj != nil and text.length > 0
    write_indent if newline
    text = XMLUtil.escape(text) if escape
    out text
  elsif newline
    out "#{@cr}"
  end

  self
end

#use_non_windows_crObject

Uses #@cr for carriage returns which is appropriate on every platform except Windows. Note that this is the default.



561
562
563
# File 'lib/wax.rb', line 561

def use_non_windows_cr
  @cr = NONWINDOWS_CR
end

#use_windows_crObject

Uses r#@cr for carriage returns which is appropriate only on the Windows platform. Note that this is not the default.



568
569
570
# File 'lib/wax.rb', line 568

def use_windows_cr
  @cr = WINDOWS_CR
end

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



53
54
55
# File 'lib/wax.rb', line 53

def write(writer=$stdout, version=nil, &proc)
  instance_eval(&proc)
end

#xslt(file_path) ⇒ Object

Writes an “xml-stylesheet” processing instruction.



666
667
668
669
670
671
672
673
674
675
676
677
678
# File 'lib/wax.rb', line 666

def xslt(file_path)
  raise "can't specify more than one XSLT" if @xslt_specified

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

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

  @xslt_specified = true
end