File: XMLElement.rb

package info (click to toggle)
tj3 3.8.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 5,048 kB
  • sloc: ruby: 36,481; javascript: 1,113; sh: 19; makefile: 17
file content (244 lines) | stat: -rw-r--r-- 6,605 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
#!/usr/bin/env ruby -w
# encoding: UTF-8
#
# = XMLElement.rb -- The TaskJuggler III Project Management Software
#
# Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014
#               by Chris Schlaeger <cs@taskjuggler.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#

require 'taskjuggler/UTF8String'

class TaskJuggler

  # This class models an XML node that may contain other XML nodes. XML element
  # trees can be constructed with the class constructor and converted into XML.
  class XMLElement

    # Construct a new XML element and include it in an existing XMLElement tree.
    def initialize(name, attributes = {}, selfClosing = false, &block)
      if (name.nil? && attributes.length > 0) ||
         (!name.nil? && !name.is_a?(String))
        raise ArgumentError, "Name must be nil or a String "
      end
      @name = name
      attributes.each do |n, v|
        if n.nil? || v.nil?
          raise ArgumentError,
            "Attribute name (#{n}) or value (#{v}) may not be nil"
        end
        unless v.is_a?(String)
          raise ArgumentError,
            "Attribute value of #{n} must be a String"
        end
      end
      @attributes = attributes
      # This can be set to true if <name /> is legal for this element.
      @selfClosing = selfClosing

      @children = block ? yield(block) : []
      # Allow blocks with single elements not to be Arrays. They will be
      # automatically converted into Arrays here.
      unless @children.is_a?(Array)
        @children = [ @children ]
      else
        @children.flatten!
      end

      # Convert all children that are text String objects into XMLText
      # objects.
      @children.collect! do |c|
        c.is_a?(String) ? XMLText.new(c) : c
      end

      # Make sure we have no nil objects in the list.
      @children.delete_if { |c| c.nil? }

      # Now all children must be XMLElement objects.
      @children.each do |c|
        unless c.is_a?(XMLElement)
          raise ArgumentError,
            "Element must be of type XMLElement, not #{c.class}: #{c.inspect}"
        end
      end
    end

    # Add a new child or a set of new childs to the element.
    def <<(arg)
      # If the argument is an array, we have to insert each element
      # individually.
      if arg.is_a?(XMLElement)
        @children << arg
      elsif arg.is_a?(String)
        @children << XMLText.new(arg)
      elsif arg.is_a?(Array)
        # Delete all nil entries
        arg.delete_if { |i| i.nil? }
        # Check that the rest are really all XMLElement objects.
        arg.each do |i|
          unless i.is_a?(XMLElement)
            raise ArgumentError,
              "Element must be of type XMLElement, not #{i.class}: #{i.inspect}"
          end
        end
        @children += arg
      elsif arg.nil?
        # Do nothing. Insertions of nil are simply ignored.
      else
        raise "Elements must be of type XMLElement not #{arg.class}"
      end
      self
    end

    # Add or change _attribute_ to _value_.
    def []=(attribute, value)
      raise ArgumentError,
        "Attribute value #{value} is not a String" unless value.is_a?(String)
      @attributes[attribute] = value
    end

    # Return the value of attribute _attribute_.
    def [](attribute)
      @attributes[attribute]
    end


    # Return the element and all sub elements as properly formatted XML.
    def to_s(indent = 0)
      out = '<' + @name
      @attributes.keys.sort.each do |attrName|
        out << " #{attrName}=\"#{escape(@attributes[attrName], true)}\""
      end
      if @children.empty? && @selfClosing
        out << '/>'
      else
        out << '>'
        @children.each do |child|
          # We only insert newlines for multiple childs and after a tag has been
          # closed.
          if @children.size > 1 && !child.is_a?(XMLText) && out[-1] == ?>
            out << "\n" + indentation(indent + 1)
          end
          out << child.to_s(indent + 1)
        end
        out << "\n" + indentation(indent) if @children.size > 1 && out[-1] == ?>
        out << '</' + @name + '>'
      end
    end

  protected

    # Escape special characters in input String _str_.
    def escape(str, quotes = false)
      out = ''
      str.each_utf8_char do |c|
        case c
        when '&'
          out << '&amp;'
        when '"'
          out << '\"'
        else
          out << c
        end
      end
      out
    end

    def indentation(indent)
      ' ' * indent
    end

  end

  # This is a specialized XMLElement to represent a simple text.
  class XMLText < XMLElement

    def initialize(text)
      super(nil, {})
      raise 'Text may not be nil' unless text
      @text = text
    end

    def to_s(indent)
      out = ''
      @text.each_utf8_char do |c|
        case c
        when '<'
          out << '&lt;'
        when '>'
          out << '&gt;'
        when '&'
          out << '&amp;'
        else
          out << c
        end
      end

      out
    end

  end

  # This is a convenience class that allows the creation of an XMLText nested
  # into an XMLElement. The _name_ and _attributes_ belong to the XMLElement,
  # the text to the XMLText.
  class XMLNamedText < XMLElement

    def initialize(text, name, attributes = {})
      super(name, attributes)
      self << XMLText.new(text)
    end

  end

  # This is a specialized XMLElement to represent a comment.
  class XMLComment < XMLElement

    def initialize(text = '')
      super(nil, {})
      @text = text
    end

    def to_s(indent)
      '<!-- ' + canonicalize_comment(@text) + " -->\n#{' ' * indent}"
    end

    private

    # It is crucial to canonicalize xml comment text because xml
    # comment syntax forbids having a -- in the comment body.  I
    # picked emacs's "M-x comment-region" approach of putting a
    # backslash between the two.
    def canonicalize_comment(text)
      new_text = text.gsub("--", "-\\-")
      new_text
    end

  end

  # This is a specialized XMLElement to represent XML blobs. The content is not
  # interpreted and must be valid XML in the content it is added.
  class XMLBlob < XMLElement

    def initialize(blob = '')
      super(nil, {})
      raise ArgumentError, "blob may not be nil" if blob.nil?
      @blob = blob
    end

    def to_s(indent)
      out = ''
      @blob.each_utf8_char do |c|
        out += (c == "\n" ? "\n" + ' ' * indent : c)
      end
      out
    end

  end

end