1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
ruby--ruby/lib/rexml/functions.rb
kou 7a6f34103d rexml: Fix XPath bug of //#{ELEMENT_NAME}[#{POSITION}]
The position should be counted for each nodeset but the previous
implementation counts position for union-ed nodeset.

For example, "/a/*/*[1]" should be matched to "<c1/>" and "<c2/>" with
the following XML.

    <a>
      <b>
        <c1/>
      </b>
      <b>
        <c2/>
      </b>
    </a>

But the previous implementation just returns only "<c1/>".

* lib/rexml/element.rb (REXML::Attributes#each_attribute):
  Support Enumerator for no block use.

* lib/rexml/element.rb (REXML::Attributes#each):
  Support Enumerator for no block use.

* lib/rexml/functions.rb (REXML::Functions.string):
  Support NaN again.

* lib/rexml/xpath_parser.rb: Re-implement "Step" evaluator.
  It should evaluate "AxisSpecifier", "NodeTest" and "Predicate" in one
  step to respect position for each nodeset.

* test/rexml/test_jaxen.rb: Enable more tests. Remained tests should
  be also enabled but it'll not be near future.

* test/rexml/xpath/test_base.rb: Fix expected value.


git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@63237 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2018-04-22 09:38:06 +00:00

447 lines
12 KiB
Ruby

# frozen_string_literal: false
module REXML
# If you add a method, keep in mind two things:
# (1) the first argument will always be a list of nodes from which to
# filter. In the case of context methods (such as position), the function
# should return an array with a value for each child in the array.
# (2) all method calls from XML will have "-" replaced with "_".
# Therefore, in XML, "local-name()" is identical (and actually becomes)
# "local_name()"
module Functions
@@available_functions = {}
@@context = nil
@@namespace_context = {}
@@variables = {}
INTERNAL_METHODS = [
:namespace_context,
:namespace_context=,
:variables,
:variables=,
:context=,
:get_namespace,
:send,
]
class << self
def singleton_method_added(name)
unless INTERNAL_METHODS.include?(name)
@@available_functions[name] = true
end
end
end
def Functions::namespace_context=(x) ; @@namespace_context=x ; end
def Functions::variables=(x) ; @@variables=x ; end
def Functions::namespace_context ; @@namespace_context ; end
def Functions::variables ; @@variables ; end
def Functions::context=(value); @@context = value; end
def Functions::text( )
if @@context[:node].node_type == :element
return @@context[:node].find_all{|n| n.node_type == :text}.collect{|n| n.value}
elsif @@context[:node].node_type == :text
return @@context[:node].value
else
return false
end
end
# Returns the last node of the given list of nodes.
def Functions::last( )
@@context[:size]
end
def Functions::position( )
@@context[:index]
end
# Returns the size of the given list of nodes.
def Functions::count( node_set )
node_set.size
end
# Since REXML is non-validating, this method is not implemented as it
# requires a DTD
def Functions::id( object )
end
# UNTESTED
def Functions::local_name( node_set=nil )
get_namespace( node_set ) do |node|
return node.local_name
end
end
def Functions::namespace_uri( node_set=nil )
get_namespace( node_set ) {|node| node.namespace}
end
def Functions::name( node_set=nil )
get_namespace( node_set ) do |node|
node.expanded_name
end
end
# Helper method.
def Functions::get_namespace( node_set = nil )
if node_set == nil
yield @@context[:node] if @@context[:node].respond_to?(:namespace)
else
if node_set.respond_to? :each
result = []
node_set.each do |node|
result << yield(node) if node.respond_to?(:namespace)
end
result
elsif node_set.respond_to? :namespace
yield node_set
end
end
end
# A node-set is converted to a string by returning the string-value of the
# node in the node-set that is first in document order. If the node-set is
# empty, an empty string is returned.
#
# A number is converted to a string as follows
#
# NaN is converted to the string NaN
#
# positive zero is converted to the string 0
#
# negative zero is converted to the string 0
#
# positive infinity is converted to the string Infinity
#
# negative infinity is converted to the string -Infinity
#
# if the number is an integer, the number is represented in decimal form
# as a Number with no decimal point and no leading zeros, preceded by a
# minus sign (-) if the number is negative
#
# otherwise, the number is represented in decimal form as a Number
# including a decimal point with at least one digit before the decimal
# point and at least one digit after the decimal point, preceded by a
# minus sign (-) if the number is negative; there must be no leading zeros
# before the decimal point apart possibly from the one required digit
# immediately before the decimal point; beyond the one required digit
# after the decimal point there must be as many, but only as many, more
# digits as are needed to uniquely distinguish the number from all other
# IEEE 754 numeric values.
#
# The boolean false value is converted to the string false. The boolean
# true value is converted to the string true.
#
# An object of a type other than the four basic types is converted to a
# string in a way that is dependent on that type.
def Functions::string( object=nil )
object = @@context[:node] if object.nil?
if object.respond_to?(:node_type)
case object.node_type
when :attribute
object.value
when :element
string_value(object)
when :document
string_value(object.root)
when :processing_instruction
object.content
else
object.to_s
end
else
case object
when Array
string(object[0])
when Float
if object.nan?
"NaN"
else
integer = object.to_i
if object == integer
"%d" % integer
else
object.to_s
end
end
when nil
""
else
object.to_s
end
end
end
# A node-set is converted to a string by
# returning the concatenation of the string-value
# of each of the children of the node in the
# node-set that is first in document order.
# If the node-set is empty, an empty string is returned.
def Functions::string_value( o )
rv = ""
o.children.each { |e|
if e.node_type == :text
rv << e.to_s
elsif e.node_type == :element
rv << string_value( e )
end
}
rv
end
def Functions::concat( *objects )
concatenated = ""
objects.each do |object|
concatenated << string(object)
end
concatenated
end
# Fixed by Mike Stok
def Functions::starts_with( string, test )
string(string).index(string(test)) == 0
end
# Fixed by Mike Stok
def Functions::contains( string, test )
string(string).include?(string(test))
end
# Kouhei fixed this
def Functions::substring_before( string, test )
ruby_string = string(string)
ruby_index = ruby_string.index(string(test))
if ruby_index.nil?
""
else
ruby_string[ 0...ruby_index ]
end
end
# Kouhei fixed this too
def Functions::substring_after( string, test )
ruby_string = string(string)
return $1 if ruby_string =~ /#{test}(.*)/
""
end
# Take equal portions of Mike Stok and Sean Russell; mix
# vigorously, and pour into a tall, chilled glass. Serves 10,000.
def Functions::substring( string, start, length=nil )
ruby_string = string(string)
ruby_length = if length.nil?
ruby_string.length.to_f
else
number(length)
end
ruby_start = number(start)
# Handle the special cases
return '' if (
ruby_length.nan? or
ruby_start.nan? or
ruby_start.infinite?
)
infinite_length = ruby_length.infinite? == 1
ruby_length = ruby_string.length if infinite_length
# Now, get the bounds. The XPath bounds are 1..length; the ruby bounds
# are 0..length. Therefore, we have to offset the bounds by one.
ruby_start = round(ruby_start) - 1
ruby_length = round(ruby_length)
if ruby_start < 0
ruby_length += ruby_start unless infinite_length
ruby_start = 0
end
return '' if ruby_length <= 0
ruby_string[ruby_start,ruby_length]
end
# UNTESTED
def Functions::string_length( string )
string(string).length
end
# UNTESTED
def Functions::normalize_space( string=nil )
string = string(@@context[:node]) if string.nil?
if string.kind_of? Array
string.collect{|x| string.to_s.strip.gsub(/\s+/um, ' ') if string}
else
string.to_s.strip.gsub(/\s+/um, ' ')
end
end
# This is entirely Mike Stok's beast
def Functions::translate( string, tr1, tr2 )
from = string(tr1)
to = string(tr2)
# the map is our translation table.
#
# if a character occurs more than once in the
# from string then we ignore the second &
# subsequent mappings
#
# if a character maps to nil then we delete it
# in the output. This happens if the from
# string is longer than the to string
#
# there's nothing about - or ^ being special in
# http://www.w3.org/TR/xpath#function-translate
# so we don't build ranges or negated classes
map = Hash.new
0.upto(from.length - 1) { |pos|
from_char = from[pos]
unless map.has_key? from_char
map[from_char] =
if pos < to.length
to[pos]
else
nil
end
end
}
if ''.respond_to? :chars
string(string).chars.collect { |c|
if map.has_key? c then map[c] else c end
}.compact.join
else
string(string).unpack('U*').collect { |c|
if map.has_key? c then map[c] else c end
}.compact.pack('U*')
end
end
# UNTESTED
def Functions::boolean( object=nil )
if object.kind_of? String
if object =~ /\d+/u
return object.to_f != 0
else
return object.size > 0
end
elsif object.kind_of? Array
object = object.find{|x| x and true}
end
return object ? true : false
end
# UNTESTED
def Functions::not( object )
not boolean( object )
end
# UNTESTED
def Functions::true( )
true
end
# UNTESTED
def Functions::false( )
false
end
# UNTESTED
def Functions::lang( language )
lang = false
node = @@context[:node]
attr = nil
until node.nil?
if node.node_type == :element
attr = node.attributes["xml:lang"]
unless attr.nil?
lang = compare_language(string(language), attr)
break
else
end
end
node = node.parent
end
lang
end
def Functions::compare_language lang1, lang2
lang2.downcase.index(lang1.downcase) == 0
end
# a string that consists of optional whitespace followed by an optional
# minus sign followed by a Number followed by whitespace is converted to
# the IEEE 754 number that is nearest (according to the IEEE 754
# round-to-nearest rule) to the mathematical value represented by the
# string; any other string is converted to NaN
#
# boolean true is converted to 1; boolean false is converted to 0
#
# a node-set is first converted to a string as if by a call to the string
# function and then converted in the same way as a string argument
#
# an object of a type other than the four basic types is converted to a
# number in a way that is dependent on that type
def Functions::number( object=nil )
object = @@context[:node] unless object
case object
when true
Float(1)
when false
Float(0)
when Array
number(string( object ))
when Numeric
object.to_f
else
str = string( object )
# If XPath ever gets scientific notation...
#if str =~ /^\s*-?(\d*\.?\d+|\d+\.)([Ee]\d*)?\s*$/
if str =~ /^\s*-?(\d*\.?\d+|\d+\.)\s*$/
str.to_f
else
(0.0 / 0.0)
end
end
end
def Functions::sum( nodes )
nodes = [nodes] unless nodes.kind_of? Array
nodes.inject(0) { |r,n| r + number(string(n)) }
end
def Functions::floor( number )
number(number).floor
end
def Functions::ceiling( number )
number(number).ceil
end
def Functions::round( number )
number = number(number)
begin
neg = number.negative?
number = number.abs.round(half: :up)
neg ? -number : number
rescue FloatDomainError
number
end
end
def Functions::processing_instruction( node )
node.node_type == :processing_instruction
end
def Functions::send(name, *args)
if @@available_functions[name.to_sym]
super
else
# TODO: Maybe, this is not XPath spec behavior.
# This behavior must be reconsidered.
XPath.match(@@context[:node], name.to_s)
end
end
end
end