# # xpath.ry # # Copyright © Ueno Katsuhiro 2000 # # $Id: xpath.ry,v 1.2 2003/03/12 06:38:21 yoshidam Exp $ #
class Compiler
prechigh
left '|' right NEG left MUL 'div' 'mod' left '+' '-' left '<' '>' '<=' '>=' left '=' '!=' left 'and' left 'or'
preclow
options no_result_var
rule
xPath: # none #
{ [] }
| expr
{
expr = val[0].expr('.to_ruby')
expr.collect! { |i| i or @context }
expr
}
| PATTERN pattern # for XSLT
{
expr = val[0].expr('.to_ruby')
expr.collect! { |i| i or @context }
expr
}
pattern: locationPath
| pattern '|' locationPath
{ val[0] ** val[2] }
expr: expr 'or' expr
{ val[0].logical_or val[2] }
| expr 'and' expr
{ val[0].logical_and val[2] }
| expr '=' expr
{ val[0].eq val[2] }
| expr '!=' expr
{ val[0].neq val[2] }
| expr '<' expr
{ val[0].lt val[2] }
| expr '>' expr
{ val[0].gt val[2] }
| expr '<=' expr
{ val[0].le val[2] }
| expr '>=' expr
{ val[0].ge val[2] }
| expr '+' expr
{ val[0] + val[2] }
| expr '-' expr
{ val[0] - val[2] }
| '-' expr =NEG
{ -val[1] }
| expr MUL expr
{ val[0] * val[2] }
| expr 'div' expr
{ val[0] / val[2] }
| expr 'mod' expr
{ val[0] % val[2] }
| expr '|' expr
{
# Why `**' is used for unionizing node-sets is that its
# precedence is higher than any other binary operators
# in Ruby.
val[0] ** val[2]
}
| locationPath
| filterExpr
| filterExpr '/' relPath
{ val[0] << val[2] }
| filterExpr '//' relPath
{ val[0].add_step('descendant-or-self') << val[2] }
filterExpr: Variable
{
Expression.new [ nil,'.get_variable(',val[0].dump,')' ]
}
| '(' expr ')'
{ val[1].unarize }
| Literal
{ Expression.new StringConstant.new(val[0]) }
| Number
{ Expression.new NumberConstant.new(val[0]) }
| functionCall
{ Expression.new val[0] }
| filterExpr predicate
{ val[0].add_predicate val[1] }
functionCall: FuncName '(' arguments ')'
{
val[2][0,0] = [ nil, ".funcall(#{val[0].dump}" ]
val[2].push(')')
}
arguments: # none #
{ [] }
| expr
{ val[0].expr.unshift ', ' }
| arguments ',' expr
{ val[0].push(', ').concat(val[2].expr) }
predicate: '['
{
c = @context
@context = c.succ
c
}
expr
{
c = @context
@context = _values[-2]
c
}
']'
{
expr = val[2]
valuetype = expr.value_type
value = expr.value
if valuetype == :number then
if value then
f = value.to_f
if f > 0 and f.truncate == f then
[ ".at(#{f.to_i})" ]
else
[ '.at(0)' ] # clear
end
else
expr.expr('.to_f').
unshift('.at(').push(')')
end
elsif value then
if value.true? then
[]
else
[ '.at(0)' ] # clear
end
else
c = val[3]
if valuetype == :ruby_boolean then
conv = '.true?'
else
conv = '.to_predicate'
end
a = expr.expr(conv)
a.collect! { |i| i or c }
a.unshift(".predicate { |#{c}| ").push(' }')
end
}
locationPath: '/'
{ LocationPath.new.absolute! }
| '/' relPath
{ val[1].absolute! }
| '//' relPath
{
path = LocationPath.new
path.absolute!
path.add_step('descendant-or-self') << val[1]
}
| relPath
relPath: step
{ LocationPath.new.add_step(*val[0]) }
| relPath '/' step
{ val[0].add_step(*val[2]) }
| relPath '//' step
{
val[0].add_step('descendant-or-self').add_step(*val[2])
}
# XPath does not permit functions here, but XPointer does.
| relPath '/' FuncName '('
{
c = @context
@context = c.succ
c
}
arguments
{
c = @context
@context = _values[-2]
c
}
')'
{
on_error unless is_xpointer?
args = val[5]
c = val[6]
args.collect! { |i| i or c }
args[0] = ".funcall(#{val[2].dump}) { |#{c}| ["
args.push '] }'
val[0].add_predicate args
}
step: '.'
{ [ 'self', false, false, false, nil ] }
| '..'
{ [ 'parent', false, false, false, nil ] }
| axisSpec nodeTest predicates
{
nodetest = val[1]
unless nodetest[0] then
axis = val[0]
if axis != 'attribute' and axis != 'namespace' then
nodetest[0] = 'element'
end
end
nodetest[0] = false if nodetest[0] == 'node'
nodetest.unshift(val[0]).push(val[2])
}
predicates: # none #
| predicates predicate
{ (val[0] || []).concat val[1] }
nodeTest: '*'
{ [ false, false, false ] }
| Name
{
if /:/ =~ val[0] then
[ false, $', $` ] #' <= for racc
else
[ false, val[0], nil ]
end
}
| Name ':' '*'
{
on_error if /:/ =~ val[0]
[ false, false, val[0] ]
}
| NodeType '(' nodeTestArg ')'
{
nodetype = val[0]
arg = val[2]
if arg and nodetype != 'processing-instruction' then
raise CompileError,
"nodetest #{nodetype}() requires no argument"
end
[ nodetype, arg || false, false ]
}
nodeTestArg: # none #
| Literal
axisSpec: # none #
{ 'child' }
| '@'
{ 'attribute' }
| AxisName '::'
end
—- inner —-
module CompilePhaseObject
def invoke_conv(expr, conv_method)
return unless conv_method
if conv_method == '.to_number' or
conv_method == '.to_string' or
conv_method == '.to_boolean' then
expr.push conv_method, '(', nil, ')'
else
expr.push conv_method
end
end
private :invoke_conv
end
module ConstantObject
include CompilePhaseObject
def to_string
StringConstant.new to_str
end
def to_number
NumberConstant.new self
end
def to_boolean
if true? then
ConstantTrue
else
ConstantFalse
end
end
end
module BooleanConstant
include ConstantObject
def value_type
:boolean
end
def expr(conv_method = nil)
if conv_method == '.to_ruby' or conv_method == '.true?' then
[ true?.to_s ]
else
ret = [ nil, '.make_boolean(', true?.to_s, ')' ]
invoke_conv ret, conv_method unless conv_method == '.to_boolean'
ret
end
end
end
class ConstantTrueClass < XPathTrueClass
include BooleanConstant
@instance = new
end
class ConstantFalseClass < XPathFalseClass
include BooleanConstant
@instance = new
end
ConstantTrue = ConstantTrueClass.instance
ConstantFalse = ConstantFalseClass.instance
class NumberConstant < XPathNumber
include ConstantObject
def value_type
:number
end
def initialize(src)
f = src.to_f
if src.is_a? ConstantObject and s = dump_float(f) then
src = s
end
@src = [ src ]
@precedence = 1
super f
end
attr_reader :precedence
protected :precedence
def to_number
self
end
def expr(conv_method = nil)
@src.collect! { |i|
if i.is_a? ConstantObject then
i.expr '.to_f'
else
i
end
}
expr = @src
expr.flatten!
@src = :draff # for debug
unless conv_method == '.to_ruby' or conv_method == '.to_f' then
expr[0, 0] = [ nil, '.make_number(' ]
expr.push(')')
invoke_conv expr, conv_method unless conv_method == '.to_number'
end
expr
end
private
def dump_float(f)
if f.finite? and f == eval(s = f.to_s) then
s
elsif f.infinite? then
if f > 0 then
'(1.0 / 0.0)'
else
'(-1.0 / 0.0)'
end
elsif f.nan? then
'(0.0 / 0.0)'
else
nil
end
end
def concat(op, other, prec)
@src.unshift('(').push(')') if @precedence < prec
if other.precedence < prec then
@src.push(op).push('(').concat(other.expr('.to_f')).push(')')
else
@src.push(op).concat(other.expr('.to_f'))
end
@precedence = prec
end
public
def self.def_arithmetic_operator(op, precedence)
module_eval <<_, __FILE__, __LINE__ + 1
def #{op}(other)
super other
if s = dump_float(@value) then
@src.clear
@src.push s
else
concat ' #{op} ', other, #{precedence}
end
self
end
_
end
def_arithmetic_operator '+', 0
def_arithmetic_operator '-', 0
def_arithmetic_operator '*', 1
def_arithmetic_operator '/', 1
class << self
undef def_arithmetic_operator
end
def %(other)
orig = @value
super other
if s = dump_float(@value) then
@src.clear
@src.push s
else
f = other.to_f
other = -other if orig % f == -@value
concat ' % ', other, 1
end
self
end
def -@
super
if s = dump_float(@value) then
@src.clear
@src.push s
else
if @src.size == 1 then
@src.unshift '-'
else
@src.unshift('-(').push(')')
end
@precedence = 1
end
self
end
end
class StringConstant < XPathString
include ConstantObject
def value_type
:string
end
def to_string
self
end
def expr(conv_method = nil)
if conv_method == '.to_ruby' or conv_method == '.to_str' then
[ @value.dump ]
else
ret = [ nil, '.make_string(', @value.dump, ')' ]
invoke_conv ret, conv_method unless conv_method == '.to_string'
ret
end
end
end
class Expression
include CompilePhaseObject
def initialize(expr)
if expr.is_a? ConstantObject then
@value = expr
else
raise "BUG" unless expr.is_a? Array
@value = nil
@valuetype = nil
@expr = expr
end
@unary = true
end
attr_reader :value
def value_type
if @value then
@value.value_type
else
@valuetype
end
end
def unarize
unless @unary then
@expr.unshift('(').push(')')
@unary = true
end
self
end
def self.def_comparison_operator(name, op)
module_eval <<_, __FILE__, __LINE__ + 1
def #{name}(other)
if @value and other.value then
if @value #{op} other.value then
@value = ConstantTrue
else
@value = ConstantFalse
end
@unary = true
else
@expr = expr.push(' #{op} ').concat(other.expr)
@valuetype = :ruby_boolean
@unary = false
end
self
end
_
end
def self.def_arithmetic_operator(*ops)
ops.each { |op|
module_eval <<_, __FILE__, __LINE__ + 1
def #{op}(other)
if @value and other.value then
@value = @value.to_number #{op} other.value.to_number
else
@expr = expr('.to_number').push(' #{op} ')
# not 'to_number', for a little speed up :-)
@expr.concat other.expr('.to_f')
@valuetype = :number
@unary = false
end
self
end
_
}
end
def_comparison_operator 'eq', '=='
def_comparison_operator 'neq', '!='
def_comparison_operator 'lt', '<'
def_comparison_operator 'gt', '>'
def_comparison_operator 'le', '<='
def_comparison_operator 'ge', '>='
def_arithmetic_operator '+', '-', '*', '/', '%'
class << self
undef def_comparison_operator
undef def_arithmetic_operator
end
def -@
if @value then
@value = -@value.to_number
else
unarize
@expr = expr('.to_number').unshift('-')
end
self
end
def logical_or(other)
if @value and @value.true? then
@value = ConstantTrue
@unary = true
@expr = @valuetype = nil
else
@expr = expr('.true?').push(' || ').concat(other.expr('.true?'))
@valuetype = :ruby_boolean
@unary = false
end
self
end
def logical_and(other)
if @value and not @value.true? then
@value = ConstantFalse
@unary = true
@expr = @valuetype = nil
else
@expr = expr('.true?').push(' && ').concat(other.expr('.true?'))
@valuetype = :ruby_boolean
@unary = false
end
self
end
def **(other)
@expr = expr.push(' ** ').concat(other.expr)
@valuetype = nil
@unary = false
self
end
def add_predicate(pred)
unarize
@expr = expr.concat(pred)
@valuetype = nil
self
end
def <<(other)
path = other.expr
path.shift # nil
path.shift # .to_nodeset
add_predicate path
end
def add_step(axis)
add_predicate [ ".step(:#{axis.tr('-','_')})" ]
end
def expr(conv_method = nil)
if @value then
ret = @value.expr(conv_method)
@value = nil
elsif @valuetype == :ruby_boolean then
ret = @expr
unless conv_method == '.to_ruby' or conv_method == '.true?' then
ret[0, 0] = [ nil, '.make_boolean(' ]
ret.push ')'
invoke_conv ret, conv_method unless conv_method == '.to_boolean'
end
elsif @valuetype == :number and conv_method == '.to_number' then
ret = @expr
elsif @valuetype == :string and conv_method == '.to_string' then
ret = @expr
elsif @valuetype == :boolean and conv_method == '.to_boolean' then
ret = @expr
else
if conv_method then
unarize
invoke_conv @expr, conv_method
end
ret = @expr
end
@expr = :draff # for debug
ret
end
end
class LocationPath
include CompilePhaseObject
def initialize
@root = false
@steps = [] # [ axis, [ tests ], predicates ]
end
attr_reader :root, :steps
protected :root, :steps
def absolute!
@root = true
self
end
def add_step(axis, nodetype = false, localpart = false,
namespace = false, predicate = nil)
if nodetype == false and localpart == false and namespace == false then
append_step axis, [], predicate
else
append_step axis, [ [ nodetype, localpart, namespace ] ], predicate
end
self
end
def <<(other)
raise "BUG" if other.root
other = other.steps
other.each { |step|
if step[0] then
append_step(*step)
else
add_predicate(step[2])
end
}
self
end
def add_predicate(pred)
@steps.push [ nil, nil, pred ]
self
end
def **(other)
unless other.is_a? LocationPath then
ret = nil
else
othersteps = other.steps
size = @steps.size
unless size == othersteps.size then
othersize = othersteps.size
if size >= othersize then
ret = (@steps[0, othersize] == othersize and self)
else
ret = (othersteps[0, size] == @steps and other)
end
else
last = @steps.pop
otherlast = othersteps.pop
if @steps == othersteps and mix_step(last, otherlast) then
ret = self
else
ret = nil
end
@steps.push last
othersteps.push otherlast
end
end
ret or Expression.new(expr) ** other
end
private
UnifiableAxes = {
'descendant' => {
'descendant-or-self' => 'descendant',
},
'descendant-or-self' => {
'child' => 'descendant',
'descendant' => 'descendant',
'descendant-or-self' => 'descendant-or-self',
},
'ancestor' => {
'ancestor-or-self' => 'ancestor',
},
'ancestor-or-self' => {
'parent' => 'ancestor',
'ancestor' => 'ancestor',
'ancestor-or-self' => 'ancestor-or-self',
},
'following-sibling' => {
'following-sibling' => 'following-sibling',
},
'preceding-sibling' => {
'preceding-sibling' => 'preceding-sibling',
},
'following' => {
'following' => 'following',
'following-sibling' => 'following',
},
'preceding' => {
'preceding' => 'preceding',
'preceding-sibling' => 'preceding',
},
'child' => {
'following-sibling' => 'child',
'preceding-sibling' => 'child',
},
}
UnifiableAxes.default = {}
def append_step(axis, test, predicate)
lastaxis, lasttest, lastpred = laststep = @steps.last
if axis == 'self' and test.empty? then
@steps.push [ nil, nil, predicate ] if predicate
elsif lastaxis and lasttest.empty? and
not lastpred and not predicate and
w = UnifiableAxes[lastaxis][axis] then
laststep[0] = w
laststep[1] = test
else
@steps.push [ axis, test, predicate ]
end
end
def mix_step(step, other)
if step[0] and step[0] == other[0] and step[2] == other[2] then
step[1].concat other[1]
step
else
nil
end
end
public
def expr(conv_method = nil)
if @root then
expr = [ nil, '.root_nodeset' ]
else
expr = [ nil, '.to_nodeset' ]
end
@steps.each { |axis,test,predicate|
if axis.nil? then # predicate only
expr.concat predicate
elsif test.empty? and not predicate then
expr.push ".select_all(:#{axis.tr('-','_')})"
else
expr.push ".step(:#{axis.tr('-','_')})"
if test.empty? then
expr.push ' { |n| n.select_all'
else
expr.push ' { |n| n.select { |i| '
test.each { |nodetype,localpart,namespace|
if nodetype then
expr.push "i.node_type == :#{nodetype.tr('-','_')}", ' && '
end
if localpart then
expr.push "i.name_localpart == #{localpart.dump}", ' && '
end
if namespace.nil? then
expr.push 'i.namespace_uri.nil?', ' && '
elsif namespace then
namespace = namespace.dump
expr.push('i.namespace_uri == ', nil,
".get_namespace(#{namespace})", ' && ')
end
expr[-1] = ' or '
}
expr[-1] = ' }'
end
expr.concat predicate if predicate
expr.push ' }'
end
}
@steps = :draff # for debug
invoke_conv expr, conv_method
expr
end
def value_type
nil
end
def value
nil
end
def unarize
self
end
def self.redirect_to_expr(*ops)
ops.each { |op|
name = op
name = op[1..-1] if op[0] == ?.
module_eval <<_, __FILE__, __LINE__ + 1
def #{name}(arg) ; Expression.new(expr) #{op} arg ; end
_
}
end
redirect_to_expr('.eq', '.neq', '.lt', '.gt', '.le', '.ge',
'+', '-', '*', '/', '%', '.logical_or', '.logical_and')
class << self
undef redirect_to_expr
end
def -@
-Expression.new(expr)
end
end
Delim = '\\s\\(\\)\\[\\]\\.@,\\/\\|\\*\\+"\'=!<>:'
Name = "[^-#{Delim}][^#{Delim}]*"
Operator = {
'@' => true, '::' => true, '(' => true, '[' => true,
:MUL => true, 'and' => true, 'or' => true, 'mod' => true, 'div' => true,
'/' => true, '//' => true, '|' => true, '+' => true,
'-' => true, '=' => true, '!=' => true, '<' => true,
'<=' => true, '>' => true, '>=' => true,
':' => false
# ':' '*' => '*' must not be a MultiplyOperator
# ':' 'and' => 'and' must be a OperatorName
}
NodeType = {
'comment' => true,
'text' => true,
'processing-instruction' => true,
'node' => true,
}
private
def axis?(s)
/\A[-a-zA-Z]+\z/ =~ s
end
def nodetype?(s)
NodeType.key? s
end
def tokenize(src)
token = []
src.scan(/(\.\.?|\/\/?|::?|!=|[<>]=?|[-()\[\].@,|+=*])|
("[^"]*"|'[^']*')|(\d+\.?\d*)|
(\$?#{Name}(?::#{Name})?)|
\s+|./ox) { |delim,literal,number,name| #/
if delim then
if delim == '*' then
delim = :MUL if (prev = token[-1]) and not Operator.key? prev[0]
elsif delim == '::' then
prev = token[-1]
if prev and prev[0] == :Name and axis? prev[1] then
prev[0] = :AxisName
end
elsif delim == '(' then
if (prev = token[-1]) and prev[0] == :Name then
if nodetype? prev[1] then
prev[0] = :NodeType
else
prev[0] = :FuncName
end
end
end
token.push [ delim, delim ]
elsif name then
prev = token[-1]
if name[0] == ?$ then
name[0,1] = ''
token.push [ :Variable, name ]
elsif Operator.key? name and
(prev = token[-1]) and not Operator[prev[0]] then
token.push [ name, name ]
else
token.push [ :Name, name ]
end
elsif number then
number << '.0' unless number.include? ?.
token.push [ :Number, number ]
elsif literal then
literal.chop!
literal[0,1] = ''
token.push [ :Literal, literal ]
else
s = $&.strip
token.push [ s, s ] unless s.empty?
end
}
token
end
public
def compile(src, pattern = false)
@token = tokenize(src)
@token.push [ false, :end ]
@token.each { |i| p i } if @yydebug
@token.reverse!
@token.push [ :PATTERN, nil ] if pattern
@context = 'context0'
ret = do_parse
ret = ret.unshift("proc { |context0| ").push(" }").join
print ">>>>\n", ret, "\n<<<<\n" if @yydebug
XPathProc.new eval(ret), src
end
def initialize(debug = false)
super()
@yydebug = debug
end
private
def next_token
@token.pop
end
def is_xpointer?
false
end
def on_error(*args) # tok, val, values
raise CompileError, 'parse error'
end
—- header —- # # xpath.rb : generated by racc #
module XPath
class Error < StandardError ; end class CompileError < Error ; end class TypeError < Error ; end class NameError < Error ; end class ArgumentError < Error ; end class InvalidOperation < Error ; end class XPathProc def initialize(proc, source) @proc = proc @source = source end attr_reader :source def call(context) @proc.call context end end def self.compile(src, pattern = false) @compiler = Compiler.new unless defined? @compiler @compiler.compile src, pattern end module XPathObject def _type type.name.sub(/\A.*::(?:XPath)?(?=[^:]+\z)/, '') end private :_type def type_error(into) raise XPath::TypeError, "failed to convert #{_type} into #{into}" end private :type_error def to_str # => to Ruby String type_error 'String' end def to_f # => to Ruby Float type_error 'Float' end def true? # => to Ruby Boolean type_error 'Boolean' end def to_ruby # => to Ruby Object self end def to_predicate # => to Ruby Float, true or false. shouldn't override. true? end def to_string(context) # => to XPath String. shouldn't override. context.make_string to_str end def to_number(context) # => to XPath Number. shouldn't override. context.make_number to_f end def to_boolean(context) # => to XPath Boolean. shouldn't override. context.make_boolean true? end public # called from compiled XPath expression def ==(other) if other.is_a? XPathNodeSet or other.is_a? XPathBoolean or other.is_a? XPathNumber then other == self else to_str == other.to_str end end def <(other) if other.is_a? XPathNodeSet then other > self else to_f < other.to_f end end def >(other) if other.is_a? XPathNodeSet then other < self else to_f > other.to_f end end def <=(other) if other.is_a? XPathNodeSet then other >= self else to_f <= other.to_f end end def >=(other) if other.is_a? XPathNodeSet then other <= self else to_f >= other.to_f end end def **(other) type_error 'NodeSet' end def predicate(&block) type_error 'NodeSet' end def at(pos) type_error 'NodeSet' end def funcall(name) # for XPointer raise XPath::NameError, "undefined function `#{name}' for #{_type}" end end class XPathBoolean include XPathObject class << self attr_reader :instance private :new end def to_str true?.to_s end # def to_f # def true? def to_ruby true? end def to_boolean(context) self end def ==(other) true? == other.true? end end class XPathTrueClass < XPathBoolean @instance = new def to_f 1.0 end def true? true end end class XPathFalseClass < XPathBoolean @instance = new def to_f 0.0 end def true? false end end XPathTrue = XPathTrueClass.instance XPathFalse = XPathFalseClass.instance class XPathNumber include XPathObject def initialize(num) raise ::TypeError, "must be a Float" unless num.is_a? Float @value = num end def to_str if @value.nan? then 'NaN' elsif @value.infinite? then if @value < 0 then '-Infinity' else 'Infinity' end else sprintf("%.100f", @value).gsub(/\.?0+\z/, '') # enough? end end def to_f @value end def true? @value != 0.0 and not @value.nan? end def to_ruby to_f end def to_predicate to_f end def to_number(context) self end def ==(other) if other.is_a? XPathNodeSet or other.is_a? XPathBoolean then other == self else @value == other.to_f end end def +(other) @value += other.to_f self end def -(other) @value -= other.to_f self end def *(other) @value *= other.to_f self end def /(other) @value /= other.to_f self end def %(other) n = other.to_f f = @value % n f = -f if @value < 0 f = -f if n < 0 @value = f self end def -@ @value = -@value self end def floor @value = @value.floor.to_f self end def ceil @value = @value.ceil.to_f self end def round f = @value unless f.nan? or f.infinite? then if f >= 0.0 then @value = f.round.to_f elsif f - f.truncate >= -0.5 then @value = f.ceil.to_f else @value = f.floor.to_f end end self end end class XPathString include XPathObject def initialize(str) raise ::TypeError, "must be a String" unless str.is_a? String @value = str end def to_str @value end def to_f if /\A\s*(-?\d+\.?\d*)(?:\s|\z)/ =~ @value then $1.to_f else 0.0 / 0.0 # NaN end end def true? not @value.empty? end def to_ruby to_str end def to_string(context) self end def concat(s) @value = @value + s self end def start_with?(s) /\A#{Regexp.quote(s)}/ =~ @value end def contain?(s) /#{Regexp.quote(s)}/ =~ @value end def substring_before(s) if /#{Regexp.quote(s)}/ =~ @value then @value = $` else @value = '' end self end def substring_after(s) if /#{Regexp.quote(s)}/ =~ @value then @value = $' else @value = '' end self end def substring(start, len) start = start.round.to_f if start.infinite? or start.nan? then @value = '' elsif len then len = len.round.to_f maxlen = start + len len = maxlen - 1.0 if len >= maxlen if start <= 1.0 then start = 0 else start = start.to_i - 1 end if len.nan? or len < 1.0 then @value = '' elsif len.infinite? then # @value = @value[start..-1] /\A[\W\w]{0,#{start}}/ =~ @value @value = $' else # @value = @value[start, len.to_i] /\A[\W\w]{0,#{start}}([\W\w]{0,#{len.to_i}})/ =~ @value @value = $1 end elsif start > 1.0 then # @value = @value[(start-1)..-1] /\A[\W\w]{0,#{start.to_i-1}}/ =~ @value @value = $' end raise "BUG" unless @value self end def size @value.gsub(/[^\Wa-zA-Z_\d]/, ' ').size end def normalize_space @value = @value.strip @value.gsub!(/\s+/, ' ') self end def translate(from, to) to = to.split(//) h = {} from.split(//).each_with_index { |i,n| h[i] = to[n] unless h.key? i } @value = @value.gsub(/[#{Regexp.quote(h.keys.join)}]/) { |s| h[s] } self end def replace(str) @value = str self end end
—- footer —-
#
# Client NodeVisitor a NodeAdapter a Node
# | | | |
# |=| | | |
# | |--{visit(node)}-->|=| | |
# | | | |---{accept(self)}----------------->|=|
# | | |=| | | |
# | | | | | |
# | | |=|<------------------{on_**(self)}---|=|
# | | | | | |
# | | | |--{wrap(node)}-->|=| |
# | | | | | | |
# | | | | |=| |
# | |<--[NodeAdapter]--|=| | |
# | | | | |
# | |-----{request}----------------------->|=| |
# | | | | |--{request}--->|=|
# | | | | | | |
# | | | | |<-----[Data]---|=|
# | |<--------------------------[Data]-----|=| |
# | | | | |
# |=| | | |
# | | | |
#
class TransparentNodeVisitor
def visit(node)
node
end
end
class NullNodeAdapter
def node
self
end
def root
nil
end
def parent
nil
end
def children
[]
end
def each_following_siblings
end
def each_preceding_siblings
end
def attributes
[]
end
def namespaces
[]
end
def index
0
end
def node_type
nil
end
def name_localpart
nil
end
def qualified_name
name_localpart
end
def namespace_uri
nil
end
def string_value
''
end
def lang
nil
end
def select_id(*ids)
raise XPath::Error, "selection by ID is not supported"
end
end
class AxisIterator
def reverse_order?
false
end
end
class ReverseAxisIterator < AxisIterator
def reverse_order?
true
end
end
class SelfIterator < AxisIterator
def each(node, visitor)
yield visitor.visit(node)
end
end
class ChildIterator < AxisIterator
def each(node, visitor, &block)
visitor.visit(node).children.each { |i| yield visitor.visit(i) }
end
end
class ParentIterator < AxisIterator
def each(node, visitor)
parent = visitor.visit(node).parent
yield visitor.visit(parent) if parent
end
end
class AncestorIterator < ReverseAxisIterator
def each(node, visitor)
node = visitor.visit(node).parent
while node
i = visitor.visit(node)
parent = i.parent
yield i
node = parent
end
end
end
class AncestorOrSelfIterator < AncestorIterator
def each(node, visitor)
yield visitor.visit(node)
super
end
end
class DescendantIterator < AxisIterator
def each(node, visitor)
stack = visitor.visit(node).children.reverse
while node = stack.pop
i = visitor.visit(node)
stack.concat i.children.reverse
yield i
end
end
end
class DescendantOrSelfIterator < DescendantIterator
def each(node, visitor)
yield visitor.visit(node)
super
end
end
class FollowingSiblingIterator < AxisIterator
def each(node, visitor)
visitor.visit(node).each_following_siblings { |i|
yield visitor.visit(i)
}
end
end
class PrecedingSiblingIterator < ReverseAxisIterator
def each(node, visitor)
visitor.visit(node).each_preceding_siblings { |i|
yield visitor.visit(i)
}
end
end
class FollowingIterator < DescendantOrSelfIterator
def each(node, visitor)
while parent = (a = visitor.visit(node)).parent
a.each_following_siblings { |i| super i, visitor }
node = parent
end
end
end
class PrecedingIterator < ReverseAxisIterator
def each(node, visitor)
while parent = (adaptor = visitor.visit(node)).parent
adaptor.each_preceding_siblings { |i|
stack = visitor.visit(i).children.dup
while node = stack.pop
a = visitor.visit(node)
stack.concat a.children
yield a
end
yield visitor.visit(i)
}
node = parent
end
end
end
class AttributeIterator < AxisIterator
def each(node, visitor)
visitor.visit(node).attributes.each { |i| yield visitor.visit(i) }
end
end
class NamespaceIterator < AxisIterator
def each(node, visitor)
visitor.visit(node).namespaces.each { |i| yield visitor.visit(i) }
end
end
class XPathNodeSet
class LocationStep < XPathNodeSet
def initialize(context)
@context = context
@visitor = context.visitor
@nodes = []
end
def set_iterator(iterator)
@iterator = iterator
end
def reuse(node)
@node = node
@nodes.clear
end
def select
@iterator.each(@node, @visitor) { |i|
node = i.node
@nodes.push node if yield(i)
}
self
end
def select_all
@iterator.each(@node, @visitor) { |i| @nodes.push i.node }
self
end
end
include XPathObject
def initialize(context, *nodes)
@context = context.dup
@visitor = context.visitor
nodes.sort! { |a,b| compare_position a, b }
@nodes = nodes
end
attr_reader :nodes
protected :nodes
def to_str
if @nodes.empty? then
''
else
@visitor.visit(@nodes[0]).string_value
end
end
def to_f
to_string(@context).to_f
end
def true?
not @nodes.empty?
end
def to_ruby
@nodes
end
def self.def_comparison_operator(*ops)
ops.each { |op|
module_eval <<_, __FILE__, __LINE__ + 1
def #{op}(other)
if other.is_a? XPathBoolean then
other #{op} self.to_boolean
else
visitor = @visitor
str = @context.make_string('')
ret = false
@nodes.each { |node|
str.replace visitor.visit(node).string_value
break if ret = (other #{op} str)
}
ret
end
end
_
}
end
def_comparison_operator '==', '<', '>', '<=', '>='
class << self
undef def_comparison_operator
end
def **(other)
super unless other.is_a? XPathNodeSet
merge other.nodes
self
end
def count
@nodes.size
end
def first
@nodes[0]
end
def each(&block)
@nodes.each(&block)
end
def funcall(name) # for XPointer
raise "BUG" unless block_given?
func = ('f_' + name.tr('-', '_')).intern
super unless respond_to? func, true
size = @nodes.size
pos = 1
c = @context.dup
begin
@nodes.collect! { |node|
c.reuse node, pos, size
pos += 1
args = yield(c)
send(func, node, *args)
}
rescue Object::ArgumentError
if $@[1] == "#{__FILE__}:#{__LINE__-3}:in `send'" then
raise XPath::ArgumentError, "#{$!} for `#{name}'"
end
raise
end
self
end
private
def compare_position(node1, node2)
visitor = @visitor
ancestors1 = []
ancestors2 = []
p1 = visitor.visit(node1).parent
while p1
ancestors1.push node1
p1 = visitor.visit(node1 = p1).parent
end
p2 = visitor.visit(node2).parent
while p2
ancestors2.push node2
p2 = visitor.visit(node2 = p2).parent
end
unless node1 == node2 then
raise XPath::Error, "can't compare the positions of given two nodes"
end
n = -1
ancestors1.reverse_each { |node1|
node2 = ancestors2[n]
unless node1 == node2 then
break unless node2
return visitor.visit(node1).index - visitor.visit(node2).index
end
n -= 1
}
ancestors1.size - ancestors2.size
end
def merge(other)
if @nodes.empty? or other.empty? then
@nodes.concat other
elsif (n = compare_position(@nodes.last, other.first)) <= 0 then
@nodes.pop if n == 0
@nodes.concat other
elsif (n = compare_position(other.last, @nodes.first)) <= 0 then
other.pop if n == 0
@nodes = other.concat(@nodes)
else
newnodes = []
nodes = @nodes
until nodes.empty? or other.empty?
n = compare_position(nodes.last, other.last)
if n > 0 then
newnodes.push nodes.pop
elsif n < 0 then
newnodes.push other.pop
else
newnodes.push nodes.pop
other.pop
end
end
newnodes.reverse!
@nodes.concat(other).concat(newnodes)
end
end
IteratorForAxis = {
:self => SelfIterator.new,
:child => ChildIterator.new,
:parent => ParentIterator.new,
:ancestor => AncestorIterator.new,
:ancestor_or_self => AncestorOrSelfIterator.new,
:descendant => DescendantIterator.new,
:descendant_or_self => DescendantOrSelfIterator.new,
:following => FollowingIterator.new,
:preceding => PrecedingIterator.new,
:following_sibling => FollowingSiblingIterator.new,
:preceding_sibling => PrecedingSiblingIterator.new,
:attribute => AttributeIterator.new,
:namespace => NamespaceIterator.new,
}
def get_iterator(axis)
ret = IteratorForAxis[axis]
unless ret then
raise XPath::NameError, "invalid axis `#{axis.id2name.tr('_','-')}'"
end
ret
end
def make_location_step
if defined? @__lstep__ then
@__lstep__
else
@__lstep__ = LocationStep.new(@context)
end
end
public
def step(axis)
iterator = get_iterator(axis)
lstep = make_location_step
lstep.set_iterator iterator
oldnodes = @nodes
@nodes = []
oldnodes.each { |node|
lstep.reuse node
nodes = yield(lstep).nodes
nodes.reverse! if iterator.reverse_order?
merge nodes
}
self
end
def select_all(axis)
iterator = get_iterator(axis)
visitor = @visitor
oldnodes = @nodes
@nodes = []
oldnodes.each { |start|
nodes = []
iterator.each(start, visitor) { |i| nodes.push i.node }
nodes.reverse! if iterator.reverse_order?
merge nodes
}
self
end
def predicate
context = @context
size = @nodes.size
pos = 1
result = nil
newnodes = @nodes.reject { |node|
context.reuse node, pos, size
pos += 1
result = yield(context)
break if result.is_a? Numeric
not result
}
if result.is_a? Numeric then
at result
else
@nodes = newnodes
end
self
end
def at(pos)
n = pos.to_i
if n != pos or n <= 0 then
node = nil
else
node = @nodes[n - 1]
end
@nodes.clear
@nodes.push node if node
self
end
end
class Context
def initialize(node, namespace = nil, variable = nil, visitor = nil)
visitor = TransparentNodeVisitor.new unless visitor
@visitor = visitor
@node = node
@context_position = 1
@context_size = 1
@variables = variable
@namespaces = namespace || {}
end
attr_reader :visitor, :node, :context_position, :context_size
def reuse(node, pos = 1, size = 1)
@variables = nil
@node, @context_position, @context_size = node, pos, size
end
def get_variable(name)
value = @variables && @variables[name] # value should be a XPathObjcect.
raise XPath::NameError, "undefined variable `#{name}'" unless value
value
end
PredefinedNamespace = {
'xml' => 'http://www.w3.org/XML/1998/namespace',
}
def get_namespace(prefix)
ret = @namespaces[prefix] || PredefinedNamespace[prefix]
raise XPath::Error, "undeclared namespace `#{prefix}'" unless ret
ret
end
def make_string(str)
XPathString.new str
end
def make_number(num)
XPathNumber.new num
end
def make_boolean(f)
if f then
XPathTrue
else
XPathFalse
end
end
def make_nodeset(*nodes)
XPathNodeSet.new(self, *nodes)
end
def to_nodeset
make_nodeset @node
end
def root_nodeset
make_nodeset @visitor.visit(@node).root
end
def funcall(name, *args)
begin
send('f_' + name.tr('-', '_'), *args)
rescue Object::NameError
if $@[0] == "#{__FILE__}:#{__LINE__-2}:in `send'" then
raise XPath::NameError, "undefined function `#{name}'"
end
raise
rescue Object::ArgumentError
if $@[1] == "#{__FILE__}:#{__LINE__-7}:in `send'" then
raise XPath::ArgumentError, "#{$!} for `#{name}'"
end
raise
end
end
private
def must(type, *args)
args.each { |i|
unless i.is_a? type then
s = type.name.sub(/\A.*::(?:XPath)?(?=[^:]+\z)/, '')
raise XPath::TypeError, "argument must be #{s}"
end
}
end
def must_be_nodeset(*args)
must XPathNodeSet, *args
end
def f_last
make_number @context_size.to_f
end
def f_position
make_number @context_position.to_f
end
def f_count(nodeset)
must_be_nodeset nodeset
make_number nodeset.count.to_f
end
def f_id(obj)
unless obj.is_a? XPathNodeSet then
ids = obj.to_str.strip.split(/\s+/)
else
ids = []
obj.each { |node| ids.push @visitor.visit(node).string_value }
end
root = @visitor.visit(@node).root
make_nodeset(*@visitor.visit(root).select_id(*ids))
end
def f_local_name(nodeset = nil)
unless nodeset then
n = @node
else
must_be_nodeset nodeset
n = nodeset.first
end
n = @visitor.visit(n) if n
n = n.name_localpart if n
n = '' unless n
make_string n
end
def f_namespace_uri(nodeset = nil)
unless nodeset then
n = @node
else
must_be_nodeset nodeset
n = nodeset.first
end
n = @visitor.visit(n) if n
n = n.namespace_uri if n
n = '' unless n
make_string n
end
def f_name(nodeset = nil)
unless nodeset then
n = @node
else
must_be_nodeset nodeset
n = nodeset.first
end
n = @visitor.visit(n) if n
n = n.qualified_name if n
n = '' unless n
make_string n
end
def f_string(obj = nil)
obj = to_nodeset unless obj
obj.to_string self
end
def f_concat(str, str2, *strs)
s = str2.to_str.dup
strs.each { |i| s << i.to_str }
str.to_string(self).concat(s)
end
def f_starts_with(str, sub)
make_boolean str.to_string(self).start_with?(sub.to_str)
end
def f_contains(str, sub)
make_boolean str.to_string(self).contain?(sub.to_str)
end
def f_substring_before(str, sub)
str.to_string(self).substring_before sub.to_str
end
def f_substring_after(str, sub)
str.to_string(self).substring_after sub.to_str
end
def f_substring(str, start, len = nil)
len = len.to_number(self) if len
str.to_string(self).substring start.to_number(self), len
end
def f_string_length(str = nil)
if str then
str = str.to_string(self)
else
str = make_string(@node.string_value)
end
make_number str.size.to_f
end
def f_normalize_space(str = nil)
if str then
str = str.to_string(self)
else
str = make_string(@node.string_value)
end
str.normalize_space
end
def f_translate(str, from, to)
str.to_string(self).translate from.to_str, to.to_str
end
def f_boolean(obj)
obj.to_boolean self
end
def f_not(bool)
make_boolean(!bool.true?)
end
def f_true
make_boolean true
end
def f_false
make_boolean false
end
def f_lang(str)
lang = @visitor.visit(@node).lang
make_boolean(lang && /\A#{Regexp.quote(str.to_str)}(?:-|\z)/i =~ lang)
end
def f_number(obj = nil)
obj = to_nodeset unless obj
obj.to_number self
end
def f_sum(nodeset)
must_be_nodeset nodeset
sum = 0.0
nodeset.each { |node|
sum += make_string(@visitor.visit(node).string_value).to_f
}
make_number sum
end
def f_floor(num)
num.to_number(self).floor
end
def f_ceiling(num)
num.to_number(self).ceil
end
def f_round(num)
num.to_number(self).round
end
end
end