Imported File#ftype spec from rubyspecs.
[rbx.git] / lib / rexml / functions.rb
blob8293e9c5ace3f10a52fadcc990ec2556766075ff
1 module REXML
2   # If you add a method, keep in mind two things:
3   # (1) the first argument will always be a list of nodes from which to
4   # filter.  In the case of context methods (such as position), the function
5   # should return an array with a value for each child in the array.
6   # (2) all method calls from XML will have "-" replaced with "_".
7   # Therefore, in XML, "local-name()" is identical (and actually becomes)
8   # "local_name()"
9   module Functions
10     @@context = nil
11     @@namespace_context = {}
12     @@variables = {}
14     def Functions::namespace_context=(x) ; @@namespace_context=x ; end
15     def Functions::variables=(x) ; @@variables=x ; end
16     def Functions::namespace_context ; @@namespace_context ; end
17     def Functions::variables ; @@variables ; end
19     def Functions::context=(value); @@context = value; end
21     def Functions::text( )
22       if @@context[:node].node_type == :element
23         return @@context[:node].find_all{|n| n.node_type == :text}.collect{|n| n.value}
24       elsif @@context[:node].node_type == :text
25         return @@context[:node].value
26       else
27         return false
28       end
29     end
31     def Functions::last( )
32       @@context[:size]
33     end
35     def Functions::position( )
36       @@context[:index]
37     end
39     def Functions::count( node_set )
40       node_set.size
41     end
43     # Since REXML is non-validating, this method is not implemented as it
44     # requires a DTD
45     def Functions::id( object )
46     end
48     # UNTESTED
49     def Functions::local_name( node_set=nil )
50       get_namespace( node_set ) do |node|
51         return node.local_name 
52       end
53     end
55     def Functions::namespace_uri( node_set=nil )
56       get_namespace( node_set ) {|node| node.namespace}
57     end
59     def Functions::name( node_set=nil )
60       get_namespace( node_set ) do |node| 
61         node.expanded_name
62       end
63     end
65     # Helper method.
66     def Functions::get_namespace( node_set = nil )
67       if node_set == nil
68         yield @@context[:node] if defined? @@context[:node].namespace
69       else  
70         if node_set.respond_to? :each
71           node_set.each { |node| yield node if defined? node.namespace }
72         elsif node_set.respond_to? :namespace
73           yield node_set
74         end
75       end
76     end
78     # A node-set is converted to a string by returning the string-value of the
79     # node in the node-set that is first in document order. If the node-set is
80     # empty, an empty string is returned.
81     #
82     # A number is converted to a string as follows
83     #
84     # NaN is converted to the string NaN 
85     #
86     # positive zero is converted to the string 0 
87     #
88     # negative zero is converted to the string 0 
89     #
90     # positive infinity is converted to the string Infinity 
91     #
92     # negative infinity is converted to the string -Infinity 
93     #
94     # if the number is an integer, the number is represented in decimal form
95     # as a Number with no decimal point and no leading zeros, preceded by a
96     # minus sign (-) if the number is negative
97     #
98     # otherwise, the number is represented in decimal form as a Number
99     # including a decimal point with at least one digit before the decimal
100     # point and at least one digit after the decimal point, preceded by a
101     # minus sign (-) if the number is negative; there must be no leading zeros
102     # before the decimal point apart possibly from the one required digit
103     # immediately before the decimal point; beyond the one required digit
104     # after the decimal point there must be as many, but only as many, more
105     # digits as are needed to uniquely distinguish the number from all other
106     # IEEE 754 numeric values.
107     #
108     # The boolean false value is converted to the string false. The boolean
109     # true value is converted to the string true.
110     #
111     # An object of a type other than the four basic types is converted to a
112     # string in a way that is dependent on that type.
113     def Functions::string( object=nil )
114       #object = @context unless object
115       if object.instance_of? Array
116         string( object[0] )
117       elsif defined? object.node_type
118         if object.node_type == :attribute
119           object.value
120         elsif object.node_type == :element || object.node_type == :document
121           string_value(object)
122         else
123           object.to_s
124         end
125       elsif object.nil?
126         return ""
127       else
128         object.to_s
129       end
130     end
132     def Functions::string_value( o )
133       rv = ""
134       o.children.each { |e|
135         if e.node_type == :text
136           rv << e.to_s
137         elsif e.node_type == :element
138           rv << string_value( e )
139         end
140       }
141       rv
142     end
144     # UNTESTED
145     def Functions::concat( *objects )
146       objects.join
147     end
149     # Fixed by Mike Stok
150     def Functions::starts_with( string, test )
151       string(string).index(string(test)) == 0
152     end
154     # Fixed by Mike Stok
155     def Functions::contains( string, test )
156       string(string).include?(string(test))
157     end
159     # Kouhei fixed this 
160     def Functions::substring_before( string, test )
161       ruby_string = string(string)
162       ruby_index = ruby_string.index(string(test))
163       if ruby_index.nil?
164         ""
165       else
166         ruby_string[ 0...ruby_index ]
167       end
168     end
170     # Kouhei fixed this too
171     def Functions::substring_after( string, test )
172       ruby_string = string(string)
173       test_string = string(test)
174       return $1 if ruby_string =~ /#{test}(.*)/
175       ""
176     end
178     # Take equal portions of Mike Stok and Sean Russell; mix 
179     # vigorously, and pour into a tall, chilled glass.  Serves 10,000.
180     def Functions::substring( string, start, length=nil )
181       ruby_string = string(string)
182       ruby_length = if length.nil? 
183                       ruby_string.length.to_f
184                     else
185                       number(length)
186                     end
187       ruby_start = number(start)
189       # Handle the special cases
190       return '' if (
191         ruby_length.nan? or 
192         ruby_start.nan? or
193         ruby_start.infinite?
194       )
196       infinite_length = ruby_length.infinite? == 1
197       ruby_length = ruby_string.length if infinite_length
198         
199       # Now, get the bounds.  The XPath bounds are 1..length; the ruby bounds 
200       # are 0..length.  Therefore, we have to offset the bounds by one.
201       ruby_start = ruby_start.round - 1
202       ruby_length = ruby_length.round
204       if ruby_start < 0
205        ruby_length += ruby_start unless infinite_length
206        ruby_start = 0
207       end
208       return '' if ruby_length <= 0
209       ruby_string[ruby_start,ruby_length]
210     end
212     # UNTESTED
213     def Functions::string_length( string )
214       string(string).length
215     end
217     # UNTESTED
218     def Functions::normalize_space( string=nil )
219       string = string(@@context[:node]) if string.nil?
220       if string.kind_of? Array
221         string.collect{|x| string.to_s.strip.gsub(/\s+/um, ' ') if string}
222       else
223         string.to_s.strip.gsub(/\s+/um, ' ')
224       end
225     end
227     # This is entirely Mike Stok's beast
228     def Functions::translate( string, tr1, tr2 )
229       from = string(tr1)
230       to = string(tr2)
232       # the map is our translation table.
233       #
234       # if a character occurs more than once in the
235       # from string then we ignore the second &
236       # subsequent mappings
237       #
238       # if a charactcer maps to nil then we delete it
239       # in the output.  This happens if the from
240       # string is longer than the to string
241       #
242       # there's nothing about - or ^ being special in
243       # http://www.w3.org/TR/xpath#function-translate
244       # so we don't build ranges or negated classes
246       map = Hash.new
247       0.upto(from.length - 1) { |pos|
248         from_char = from[pos]
249         unless map.has_key? from_char
250           map[from_char] = 
251           if pos < to.length
252             to[pos]
253           else
254             nil
255           end
256         end
257       }
259       string(string).unpack('U*').collect { |c|
260         if map.has_key? c then map[c] else c end
261       }.compact.pack('U*')
262     end
264     # UNTESTED
265     def Functions::boolean( object=nil )
266       if object.kind_of? String
267         if object =~ /\d+/u
268           return object.to_f != 0
269         else
270           return object.size > 0
271         end
272       elsif object.kind_of? Array
273         object = object.find{|x| x and true}
274       end
275       return object ? true : false
276     end
278     # UNTESTED
279     def Functions::not( object )
280       not boolean( object )
281     end
283     # UNTESTED
284     def Functions::true( )
285       true
286     end
288     # UNTESTED
289     def Functions::false(  )
290       false
291     end
293     # UNTESTED
294     def Functions::lang( language )
295       lang = false
296       node = @@context[:node]
297       attr = nil
298       until node.nil?
299         if node.node_type == :element
300           attr = node.attributes["xml:lang"]
301           unless attr.nil?
302             lang = compare_language(string(language), attr)
303             break
304           else
305           end
306         end
307         node = node.parent
308       end
309       lang
310     end
312     def Functions::compare_language lang1, lang2
313       lang2.downcase.index(lang1.downcase) == 0
314     end
316     # a string that consists of optional whitespace followed by an optional
317     # minus sign followed by a Number followed by whitespace is converted to
318     # the IEEE 754 number that is nearest (according to the IEEE 754
319     # round-to-nearest rule) to the mathematical value represented by the
320     # string; any other string is converted to NaN
321     #
322     # boolean true is converted to 1; boolean false is converted to 0
323     #
324     # a node-set is first converted to a string as if by a call to the string
325     # function and then converted in the same way as a string argument
326     #
327     # an object of a type other than the four basic types is converted to a
328     # number in a way that is dependent on that type
329     def Functions::number( object=nil )
330       object = @@context[:node] unless object
331       case object
332       when true
333         Float(1)
334       when false
335         Float(0)
336       when Array
337         number(string( object ))
338       when Numeric
339         object.to_f
340       else
341         str = string( object )
342         # If XPath ever gets scientific notation...
343         #if str =~ /^\s*-?(\d*\.?\d+|\d+\.)([Ee]\d*)?\s*$/
344         if str =~ /^\s*-?(\d*\.?\d+|\d+\.)\s*$/
345           str.to_f
346         else
347           (0.0 / 0.0)
348         end
349       end
350     end
352     def Functions::sum( nodes )
353       nodes = [nodes] unless nodes.kind_of? Array
354       nodes.inject(0) { |r,n| r += number(string(n)) }
355     end
356     
357     def Functions::floor( number )
358       number(number).floor
359     end
361     def Functions::ceiling( number )
362       number(number).ceil
363     end
365     def Functions::round( number )
366       begin
367         number(number).round
368       rescue FloatDomainError
369         number(number)
370       end
371     end
373     def Functions::processing_instruction( node )
374       node.node_type == :processing_instruction
375     end
377     def Functions::method_missing( id )
378       puts "METHOD MISSING #{id.id2name}"
379       XPath.match( @@context[:node], id.id2name )
380     end
381   end