1require "rexml/namespace"
2require 'rexml/text'
3
4module REXML
5  # Defines an Element Attribute; IE, a attribute=value pair, as in:
6  # <element attribute="value"/>.  Attributes can be in their own
7  # namespaces.  General users of REXML will not interact with the
8  # Attribute class much.
9  class Attribute
10    include Node
11    include Namespace
12
13    # The element to which this attribute belongs
14    attr_reader :element
15    # The normalized value of this attribute.  That is, the attribute with
16    # entities intact.
17    attr_writer :normalized
18    PATTERN = /\s*(#{NAME_STR})\s*=\s*(["'])(.*?)\2/um
19
20    NEEDS_A_SECOND_CHECK = /(<|&((#{Entity::NAME});|(#0*((?:\d+)|(?:x[a-fA-F0-9]+)));)?)/um
21
22    # Constructor.
23    # FIXME: The parser doesn't catch illegal characters in attributes
24    #
25    # first::
26    #   Either: an Attribute, which this new attribute will become a
27    #   clone of; or a String, which is the name of this attribute
28    # second::
29    #   If +first+ is an Attribute, then this may be an Element, or nil.
30    #   If nil, then the Element parent of this attribute is the parent
31    #   of the +first+ Attribute.  If the first argument is a String,
32    #   then this must also be a String, and is the content of the attribute.
33    #   If this is the content, it must be fully normalized (contain no
34    #   illegal characters).
35    # parent::
36    #   Ignored unless +first+ is a String; otherwise, may be the Element
37    #   parent of this attribute, or nil.
38    #
39    #
40    #  Attribute.new( attribute_to_clone )
41    #  Attribute.new( attribute_to_clone, parent_element )
42    #  Attribute.new( "attr", "attr_value" )
43    #  Attribute.new( "attr", "attr_value", parent_element )
44    def initialize( first, second=nil, parent=nil )
45      @normalized = @unnormalized = @element = nil
46      if first.kind_of? Attribute
47        self.name = first.expanded_name
48        @unnormalized = first.value
49        if second.kind_of? Element
50          @element = second
51        else
52          @element = first.element
53        end
54      elsif first.kind_of? String
55        @element = parent
56        self.name = first
57        @normalized = second.to_s
58      else
59        raise "illegal argument #{first.class.name} to Attribute constructor"
60      end
61    end
62
63    # Returns the namespace of the attribute.
64    #
65    #  e = Element.new( "elns:myelement" )
66    #  e.add_attribute( "nsa:a", "aval" )
67    #  e.add_attribute( "b", "bval" )
68    #  e.attributes.get_attribute( "a" ).prefix   # -> "nsa"
69    #  e.attributes.get_attribute( "b" ).prefix   # -> "elns"
70    #  a = Attribute.new( "x", "y" )
71    #  a.prefix                                   # -> ""
72    def prefix
73      pf = super
74      if pf == ""
75        pf = @element.prefix if @element
76      end
77      pf
78    end
79
80    # Returns the namespace URL, if defined, or nil otherwise
81    #
82    #  e = Element.new("el")
83    #  e.add_attributes({"xmlns:ns", "http://url"})
84    #  e.namespace( "ns" )              # -> "http://url"
85    def namespace arg=nil
86      arg = prefix if arg.nil?
87      @element.namespace arg
88    end
89
90    # Returns true if other is an Attribute and has the same name and value,
91    # false otherwise.
92    def ==( other )
93      other.kind_of?(Attribute) and other.name==name and other.value==value
94    end
95
96    # Creates (and returns) a hash from both the name and value
97    def hash
98      name.hash + value.hash
99    end
100
101    # Returns this attribute out as XML source, expanding the name
102    #
103    #  a = Attribute.new( "x", "y" )
104    #  a.to_string     # -> "x='y'"
105    #  b = Attribute.new( "ns:x", "y" )
106    #  b.to_string     # -> "ns:x='y'"
107    def to_string
108      if @element and @element.context and @element.context[:attribute_quote] == :quote
109        %Q^#@expanded_name="#{to_s().gsub(/"/, '&quote;')}"^
110      else
111        "#@expanded_name='#{to_s().gsub(/'/, '&apos;')}'"
112      end
113    end
114
115    def doctype
116      if @element
117        doc = @element.document
118        doc.doctype if doc
119      end
120    end
121
122    # Returns the attribute value, with entities replaced
123    def to_s
124      return @normalized if @normalized
125
126      @normalized = Text::normalize( @unnormalized, doctype )
127      @unnormalized = nil
128      @normalized
129    end
130
131    # Returns the UNNORMALIZED value of this attribute.  That is, entities
132    # have been expanded to their values
133    def value
134      return @unnormalized if @unnormalized
135      @unnormalized = Text::unnormalize( @normalized, doctype )
136      @normalized = nil
137      @unnormalized
138    end
139
140    # Returns a copy of this attribute
141    def clone
142      Attribute.new self
143    end
144
145    # Sets the element of which this object is an attribute.  Normally, this
146    # is not directly called.
147    #
148    # Returns this attribute
149    def element=( element )
150      @element = element
151
152      if @normalized
153        Text.check( @normalized, NEEDS_A_SECOND_CHECK, doctype )
154      end
155
156      self
157    end
158
159    # Removes this Attribute from the tree, and returns true if successfull
160    #
161    # This method is usually not called directly.
162    def remove
163      @element.attributes.delete self.name unless @element.nil?
164    end
165
166    # Writes this attribute (EG, puts 'key="value"' to the output)
167    def write( output, indent=-1 )
168      output << to_string
169    end
170
171    def node_type
172      :attribute
173    end
174
175    def inspect
176      rv = ""
177      write( rv )
178      rv
179    end
180
181    def xpath
182      path = @element.xpath
183      path += "/@#{self.expanded_name}"
184      return path
185    end
186  end
187end
188#vim:ts=2 sw=2 noexpandtab:
189