Class: Oga::XPath::Evaluator

Inherits:
Object
  • Object
show all
Defined in:
lib/oga/xpath/evaluator.rb

Overview

The Evaluator class evaluates XPath expressions, either as a String or an AST of AST::Node instances.

Thread Safety

This class is not thread-safe, you can not share the same instance between multiple threads. This is due to the use of an internal stack (see below for more information). It is however perfectly fine to use multiple separated instances as this class does not use a thread global state.

Node Set Stack

This class uses an internal stack of XML node sets. This stack is used for functions that require access to the set of nodes a predicate belongs to. An example of such a function is position().

An alternative would be to pass the node sets a predicate belongs to as an extra argument to the various on_* methods. The problematic part of this approach is that it requires every method to take and pass along the argument. It's far too easy to make mistakes in such a setup and as such I've chosen to use an internal stack instead.

See #with_node_set and #current_node_set for more information.

Set Indices

XPath node sets start at index 1 instead of index 0. In other words, if you want to access the first node in a set you have to use index 1, not 0. Certain methods such as #on_call_last and #on_call_position take care of converting indices from Ruby to XPath.

Number Types

The XPath specification states that all numbers produced by an expression should be returned as double-precision 64bit IEEE 754 floating point numbers. For example, the return value of position() should be a float (e.g. "1.0", not "1").

Oga takes care internally of converting numbers to integers and/or floats where needed. The output types however will always be floats.

For more information on the specification, see http://www.w3.org/TR/xpath/#numbers.

Variables

The evaluator supports the binding of custom variables in the #initialize method. Variables can be bound by passing in a Hash with the keys set to the variable names (minus the $ sign) and their values to the variable values. The keys of the variables Hash must be Strings.

A basic example:

evaluator = Evaluator.new(document, 'number' => 10)

evaluator.evaluate('$number') # => 10

Instance Method Summary collapse

Constructor Details

#initialize(document, variables = {}) ⇒ Evaluator


67
68
69
70
71
# File 'lib/oga/xpath/evaluator.rb', line 67

def initialize(document, variables = {})
  @document  = document
  @variables = variables
  @node_sets = []
end

Instance Method Details

#child_nodes(nodes) ⇒ Oga::XML::NodeSet

Returns a node set containing all the child nodes of the given set of nodes.


1605
1606
1607
1608
1609
1610
1611
1612
1613
# File 'lib/oga/xpath/evaluator.rb', line 1605

def child_nodes(nodes)
  children = XML::NodeSet.new

  nodes.each do |xml_node|
    children.concat(xml_node.children)
  end

  return children
end

#current_node_setOga::XML::NodeSet


1761
1762
1763
# File 'lib/oga/xpath/evaluator.rb', line 1761

def current_node_set
  return @node_sets.last
end

#evaluate(string) ⇒ Mixed

Evaluates an XPath expression as a String.

Examples:

evaluator = Oga::XPath::Evaluator.new(document)

evaluator.evaluate('//a')

84
85
86
87
88
# File 'lib/oga/xpath/evaluator.rb', line 84

def evaluate(string)
  ast = Parser.new(string).parse

  return evaluate_ast(ast)
end

#evaluate_ast(ast) ⇒ Mixed

Evaluates a pre-parsed XPath expression.


96
97
98
99
100
# File 'lib/oga/xpath/evaluator.rb', line 96

def evaluate_ast(ast)
  context = XML::NodeSet.new([@document])

  return process(ast, context)
end

#first_node_text(set) ⇒ String

Returns the text of the first node in the node set, or an empty string if the node set is empty.


1594
1595
1596
# File 'lib/oga/xpath/evaluator.rb', line 1594

def first_node_text(set)
  return set[0].respond_to?(:text) ? set[0].text : ''
end

#function_node(context, expression = nil) ⇒ Oga::XML::Node

Returns the node for a function call. This node is either the first node in the supplied node set, or the first node in the current context.


1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
# File 'lib/oga/xpath/evaluator.rb', line 1571

def function_node(context, expression = nil)
  if expression
    node = process(expression, context)

    if node.is_a?(XML::NodeSet)
      node = node.first
    else
      raise TypeError, 'only node sets can be used as arguments'
    end
  else
    node = context.first
  end

  return node
end

#has_parent?(ast_node) ⇒ TrueClass|FalseClass


1704
1705
1706
# File 'lib/oga/xpath/evaluator.rb', line 1704

def has_parent?(ast_node)
  return ast_node.respond_to?(:parent) && !!ast_node.parent
end

#name_matches?(xml_node, name) ⇒ Boolean

Returns true if the name of the XML node matches the given name or matches a wildcard.


1681
1682
1683
1684
1685
# File 'lib/oga/xpath/evaluator.rb', line 1681

def name_matches?(xml_node, name)
  return false unless xml_node.respond_to?(:name)

  return xml_node.name == name || name == '*'
end

#namespace_matches?(xml_node, ns) ⇒ Boolean

Returns true if the namespace of the XML node matches the given namespace or matches a wildcard.


1694
1695
1696
1697
1698
# File 'lib/oga/xpath/evaluator.rb', line 1694

def namespace_matches?(xml_node, ns)
  return false unless xml_node.respond_to?(:namespace)

  return xml_node.namespace.to_s == ns || ns == '*'
end

#node_matches?(xml_node, ast_node) ⇒ Oga::XML::NodeSet

Checks if a given Oga::XML::Node instance matches a AST::Node instance.

This method can use both "test" and "type-test" nodes. In case of "type-test" nodes the procedure is as following:

  1. Evaluate the expression
  2. If the return value is non empty return true, otherwise return false

For "test" nodes the procedure is as following instead:

  1. Match the name
  2. Match the namespace

For both the name and namespace a wildcard (*) can be used.


1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
# File 'lib/oga/xpath/evaluator.rb', line 1637

def node_matches?(xml_node, ast_node)
  ns, name = *ast_node

  if ast_node.type == :type_test
    return type_matches?(xml_node, ast_node)
  end

  # If only the name is given and is a wildcard then we'll also want to
  # match the namespace as a wildcard.
  if !ns and name == '*'
    ns = '*'
  end

  name_matches = name_matches?(xml_node, name)
  ns_matches   = false

  if ns
    ns_matches = namespace_matches?(xml_node, ns)

  elsif name_matches and !xml_node.namespace
    ns_matches = true
  end

  return name_matches && ns_matches
end

#on_absolute_path(ast_node, context) ⇒ Oga::XML::NodeSet

Processes an absolute XPath expression such as /foo.


127
128
129
130
131
132
133
134
135
136
# File 'lib/oga/xpath/evaluator.rb', line 127

def on_absolute_path(ast_node, context)
  if @document.respond_to?(:root_node)
    context = XML::NodeSet.new([@document.root_node])
  else
    context = XML::NodeSet.new([@document])
  end

  # If the expression is just "/" we'll just return the current context.
  return ast_node.children.empty? ? context : on_path(ast_node, context)
end

#on_add(ast_node, context) ⇒ Float

Processes the + operator.

This operator converts the left and right expressions to numbers and adds them together.


690
691
692
693
694
# File 'lib/oga/xpath/evaluator.rb', line 690

def on_add(ast_node, context)
  left, right = *ast_node

  return on_call_number(context, left) + on_call_number(context, right)
end

#on_and(ast_node, context) ⇒ TrueClass|FalseClass

Processes the and operator.

This operator returns true if both the left and right expression evaluate to true. If the first expression evaluates to false the right expression is ignored.


657
658
659
660
661
# File 'lib/oga/xpath/evaluator.rb', line 657

def on_and(ast_node, context)
  left, right = *ast_node

  return on_call_boolean(context, left) && on_call_boolean(context, right)
end

#on_axis(ast_node, context) ⇒ Oga::XML::NodeSet

Dispatches the processing of axes to dedicated methods. This works similar to #process except the handler names are "on_axis_X" with "X" being the axis name.


231
232
233
234
235
236
237
# File 'lib/oga/xpath/evaluator.rb', line 231

def on_axis(ast_node, context)
  name, test = *ast_node

  handler = name.gsub('-', '_')

  return send("on_axis_#{handler}", test, context)
end

#on_axis_ancestor(ast_node, context) ⇒ Oga::XML::NodeSet

Processes the ancestor axis. This axis walks through the entire ancestor chain until a matching node is found.

Evaluation happens using a "short-circuit" mechanism. The moment a matching node is found it is returned immediately.


250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/oga/xpath/evaluator.rb', line 250

def on_axis_ancestor(ast_node, context)
  nodes = XML::NodeSet.new

  context.each do |xml_node|
    while has_parent?(xml_node)
      xml_node = xml_node.parent

      if node_matches?(xml_node, ast_node)
        nodes << xml_node
        break
      end
    end
  end

  return nodes
end

#on_axis_ancestor_or_self(ast_node, context) ⇒ Object

Processes the ancestor-or-self axis.

See Also:

  • Oga::XPath::Evaluator.[[#on_axis_ancestor]

272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/oga/xpath/evaluator.rb', line 272

def on_axis_ancestor_or_self(ast_node, context)
  nodes = XML::NodeSet.new

  context.each do |xml_node|
    while has_parent?(xml_node)
      if node_matches?(xml_node, ast_node)
        nodes << xml_node
        break
      end

      xml_node = xml_node.parent
    end
  end

  return nodes
end

#on_axis_attribute(ast_node, context) ⇒ Oga::XML::NodeSet

Processes the attribute axis. The node test is performed against all the attributes of the nodes in the current context.

Evaluation of the nodes continues until the node set has been exhausted (unlike some other methods which return the moment they find a matching node).


301
302
303
304
305
306
307
308
309
310
311
# File 'lib/oga/xpath/evaluator.rb', line 301

def on_axis_attribute(ast_node, context)
  nodes = XML::NodeSet.new

  context.each do |xml_node|
    next unless xml_node.is_a?(XML::Element)

    nodes += on_test(ast_node, xml_node.attributes)
  end

  return nodes
end

#on_axis_child(ast_node, context) ⇒ Oga::XML::NodeSet

Evaluates the child axis. This axis simply takes all the child nodes of the current context nodes.


321
322
323
# File 'lib/oga/xpath/evaluator.rb', line 321

def on_axis_child(ast_node, context)
  return process(ast_node, child_nodes(context))
end

#on_axis_descendant(ast_node, context) ⇒ Oga::XML::NodeSet

Evaluates the descendant axis. This method processes child nodes until the very end of the tree, no "short-circuiting" mechanism is used.


333
334
335
336
337
338
339
340
341
342
343
# File 'lib/oga/xpath/evaluator.rb', line 333

def on_axis_descendant(ast_node, context)
  nodes = XML::NodeSet.new

  context.each do |context_node|
    context_node.each_node do |node|
      nodes.concat(process(ast_node, XML::NodeSet.new([node])))
    end
  end

  return nodes
end

#on_axis_descendant_or_self(ast_node, context) ⇒ Oga::XML::NodeSet

Evaluates the descendant-or-self axis.


352
353
354
355
356
357
358
# File 'lib/oga/xpath/evaluator.rb', line 352

def on_axis_descendant_or_self(ast_node, context)
  nodes = on_test(ast_node, context)

  nodes.concat(on_axis_descendant(ast_node, context))

  return nodes
end

#on_axis_following(ast_node, context) ⇒ Oga::XML::NodeSet

Evaluates the following axis.


367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/oga/xpath/evaluator.rb', line 367

def on_axis_following(ast_node, context)
  nodes = XML::NodeSet.new
  root  = root_node(@document)

  context.each do |context_node|
    check = false

    root.each_node do |doc_node|
      # Skip child nodes of the current context node, compare all
      # following nodes.
      if doc_node == context_node
        check = true
        throw :skip_children
      end

      next unless check

      nodes << doc_node if node_matches?(doc_node, ast_node)
    end
  end

  return nodes
end

#on_axis_following_sibling(ast_node, context) ⇒ Oga::XML::NodeSet

Evaluates the following-sibling axis.


398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/oga/xpath/evaluator.rb', line 398

def on_axis_following_sibling(ast_node, context)
  nodes = XML::NodeSet.new
  root  = parent_node(@document)

  context.each do |context_node|
    check  = false
    parent = has_parent?(context_node) ? context_node.parent : nil

    root.each_node do |doc_node|
      # Skip child nodes of the current context node, compare all
      # following nodes.
      if doc_node == context_node
        check = true
        throw :skip_children
      end

      if !check or parent != doc_node.parent
        next
      end

      if node_matches?(doc_node, ast_node)
        nodes << doc_node

        throw :skip_children
      end
    end
  end

  return nodes
end

#on_axis_namespace(ast_node, context) ⇒ Oga::XML::NodeSet

Evaluates the namespace axis.


529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
# File 'lib/oga/xpath/evaluator.rb', line 529

def on_axis_namespace(ast_node, context)
  nodes = XML::NodeSet.new
  name  = ast_node.children[1]

  context.each do |context_node|
    next unless context_node.respond_to?(:available_namespaces)

    context_node.available_namespaces.each do |_, namespace|
      if namespace.name == name or name == '*'
        nodes << namespace
      end
    end
  end

  return nodes
end

#on_axis_parent(ast_node, context) ⇒ Oga::XML::NodeSet

Evaluates the parent axis.


436
437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/oga/xpath/evaluator.rb', line 436

def on_axis_parent(ast_node, context)
  nodes = XML::NodeSet.new

  context.each do |context_node|
    next unless has_parent?(context_node)

    parent = context_node.parent

    nodes << parent if node_matches?(parent, ast_node)
  end

  return nodes
end

#on_axis_preceding(ast_node, context) ⇒ Oga::XML::NodeSet

Evaluates the preceding axis.


457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'lib/oga/xpath/evaluator.rb', line 457

def on_axis_preceding(ast_node, context)
  nodes = XML::NodeSet.new
  root  = root_node(@document)

  context.each do |context_node|
    check = true

    root.each_node do |doc_node|
      # Test everything *until* we hit the current context node.
      if doc_node == context_node
        break
      elsif node_matches?(doc_node, ast_node)
        nodes << doc_node
      end
    end
  end

  return nodes
end

#on_axis_preceding_sibling(ast_node, context) ⇒ Oga::XML::NodeSet

Evaluates the preceding-sibling axis.


484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# File 'lib/oga/xpath/evaluator.rb', line 484

def on_axis_preceding_sibling(ast_node, context)
  nodes = XML::NodeSet.new
  root  = parent_node(@document)

  context.each do |context_node|
    check  = true
    parent = has_parent?(context_node) ? context_node.parent : nil

    root.each_node do |doc_node|
      # Test everything *until* we hit the current context node.
      if doc_node == context_node
        break
      elsif doc_node.parent == parent and node_matches?(doc_node, ast_node)
        nodes << doc_node
      end
    end
  end

  return nodes
end

#on_axis_self(ast_node, context) ⇒ Oga::XML::NodeSet

Evaluates the self axis.


512
513
514
515
516
517
518
519
520
# File 'lib/oga/xpath/evaluator.rb', line 512

def on_axis_self(ast_node, context)
  nodes = XML::NodeSet.new

  context.each do |context_node|
    nodes << context_node if node_matches?(context_node, ast_node)
  end

  return nodes
end

#on_call(ast_node, context) ⇒ Oga::XML::NodeSet

Delegates function calls to specific handlers.

Handler functions take two arguments:

  1. The context node set
  2. A variable list of XPath function arguments, passed as individual Ruby method arguments.

887
888
889
890
891
892
893
# File 'lib/oga/xpath/evaluator.rb', line 887

def on_call(ast_node, context)
  name, *args = *ast_node

  handler = name.gsub('-', '_')

  return send("on_call_#{handler}", context, *args)
end

#on_call_boolean(context, expression) ⇒ TrueClass|FalseClass

Processes the boolean() function call.

This function converts the 1st argument to a boolean.

The boolean true is returned for the following:

  • A non empty string
  • A non empty node set
  • A non zero number, either positive or negative

The boolean false is returned for all other cases.


1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
# File 'lib/oga/xpath/evaluator.rb', line 1348

def on_call_boolean(context, expression)
  retval = process(expression, context)
  bool   = false

  if retval.is_a?(Numeric)
    bool = !retval.nan? && !retval.zero?
  elsif retval
    bool = !retval.respond_to?(:empty?) || !retval.empty?
  end

  return bool
end

#on_call_ceiling(context, expression) ⇒ Float

Processes the ceiling() function call.

This function call rounds the 1st argument up to the closest integer, and then returns that number as a float.


1489
1490
1491
1492
1493
# File 'lib/oga/xpath/evaluator.rb', line 1489

def on_call_ceiling(context, expression)
  number = on_call_number(context, expression)

  return number.nan? ? number : number.ceil.to_f
end

#on_call_concat(context, first, second, *rest) ⇒ Object

Processes the concat() function call.

This function call converts its arguments to strings and concatenates them. In case of node sets the text of the set is used.


1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
# File 'lib/oga/xpath/evaluator.rb', line 1125

def on_call_concat(context, first, second, *rest)
  args   = [first, second] + rest
  retval = ''

  args.each do |arg|
    retval << on_call_string(context, arg)
  end

  return retval
end

#on_call_contains(context, haystack, needle) ⇒ String

Processes the contains() function call.

This function call returns true if the string in the 1st argument contains the string in the 2nd argument. Node sets can also be used.

Examples:

contains("hello world", "o w") # => true

1172
1173
1174
1175
1176
1177
# File 'lib/oga/xpath/evaluator.rb', line 1172

def on_call_contains(context, haystack, needle)
  haystack_str = on_call_string(context, haystack)
  needle_str   = on_call_string(context, needle)

  return haystack_str.include?(needle_str)
end

#on_call_count(context, expression) ⇒ Float

Processes the count() function call. This function counts the amount of nodes in expression and returns the result as a float.


928
929
930
931
932
933
934
935
936
# File 'lib/oga/xpath/evaluator.rb', line 928

def on_call_count(context, expression)
  retval = process(expression, context)

  unless retval.is_a?(XML::NodeSet)
    raise TypeError, 'count() can only operate on NodeSet instances'
  end

  return retval.length.to_f
end

#on_call_false(context) ⇒ FalseClass

Processes the false() function call.

This function simply returns the boolean false.


1396
1397
1398
# File 'lib/oga/xpath/evaluator.rb', line 1396

def on_call_false(context)
  return false
end

#on_call_floor(context, expression) ⇒ Float

Processes the floor() function call.

This function call rounds the 1st argument down to the closest integer, and then returns that number as a float.


1473
1474
1475
1476
1477
# File 'lib/oga/xpath/evaluator.rb', line 1473

def on_call_floor(context, expression)
  number = on_call_number(context, expression)

  return number.nan? ? number : number.floor.to_f
end

#on_call_id(context, expression) ⇒ Oga::XML::NodeSet

Processes the id() function call.

The XPath specification states that this function's behaviour should be controlled by a DTD. If a DTD were to specify that the ID attribute for a certain element would be "foo" then this function should use said attribute.

Oga does not support DTD parsing/evaluation and as such always uses the "id" attribute.

This function searches the entire document for a matching node, regardless of the current position.


956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
# File 'lib/oga/xpath/evaluator.rb', line 956

def on_call_id(context, expression)
  id    = process(expression, context)
  nodes = XML::NodeSet.new

  # Based on Nokogiri's/libxml behaviour it appears that when using a node
  # set the text of the set is used as the ID.
  id  = id.is_a?(XML::NodeSet) ? id.text : id.to_s
  ids = id.split(' ')

  @document.each_node do |node|
    next unless node.is_a?(XML::Element)

    attr = node.attribute('id')

    if attr and ids.include?(attr.value)
      nodes << node
    end
  end

  return nodes
end

#on_call_lang(context, language) ⇒ TrueClass|FalseClass

Processes the lang() function call.

This function returns true if the current context node is in the given language, false otherwise.

The language is based on the value of the "xml:lang" attribute of either the context node or an ancestor node (in case the context node has no such attribute).


1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
# File 'lib/oga/xpath/evaluator.rb', line 1414

def on_call_lang(context, language)
  lang_str = on_call_string(context, language)
  node     = context.first

  while node.respond_to?(:attribute)
    found = node.attribute('xml:lang')

    return found.value == lang_str if found

    node = node.parent
  end

  return false
end

#on_call_last(context) ⇒ Float

Processes the last() function call. This function call returns the index of the last node in the current set.


902
903
904
905
# File 'lib/oga/xpath/evaluator.rb', line 902

def on_call_last(context)
  # XPath uses indexes 1 to N instead of 0 to N.
  return current_node_set.length.to_f
end

#on_call_local_name(context, expression = nil) ⇒ Oga::XML::NodeSet

Processes the local-name() function call.

This function call returns the name of one of the following:

  • The current context node (if any)
  • The first node in the supplied node set

990
991
992
993
994
# File 'lib/oga/xpath/evaluator.rb', line 990

def on_call_local_name(context, expression = nil)
  node = function_node(context, expression)

  return node.respond_to?(:name) ? node.name : ''
end

#on_call_name(context, expression = nil) ⇒ Oga::XML::NodeSet

Processes the name() function call.

This function call is similar to local-name() (see #on_call_local_name) except that it includes the namespace name if present.


1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
# File 'lib/oga/xpath/evaluator.rb', line 1007

def on_call_name(context, expression = nil)
  node = function_node(context, expression)

  if node.respond_to?(:name) and node.respond_to?(:namespace)
    if node.namespace
      return "#{node.namespace.name}:#{node.name}"
    else
      return node.name
    end
  else
    return ''
  end
end

#on_call_namespace_uri(context, expression = nil) ⇒ Oga::XML::NodeSet

Processes the namespace-uri() function call.

This function call returns the namespace URI of one of the following:

  • The current context node (if any)
  • The first node in the supplied node set

1033
1034
1035
1036
1037
1038
1039
1040
1041
# File 'lib/oga/xpath/evaluator.rb', line 1033

def on_call_namespace_uri(context, expression = nil)
  node = function_node(context, expression)

  if node.respond_to?(:namespace) and node.namespace
    return node.namespace.uri
  else
    return ''
  end
end

#on_call_normalize_space(context, expression = nil) ⇒ String

Processes the normalize-space() function call.

This function strips the 1st argument string or the current context node of leading/trailing whitespace as well as replacing multiple whitespace sequences with single spaces.

Examples:

normalize-space(" fo  o    ") # => "fo o"

1296
1297
1298
1299
1300
# File 'lib/oga/xpath/evaluator.rb', line 1296

def on_call_normalize_space(context, expression = nil)
  str = on_call_string(context, expression)

  return str.strip.gsub(/\s+/, ' ')
end

#on_call_not(context, expression) ⇒ TrueClass|FalseClass

Processes the not() function call.

This function converts the 1st argument to a boolean and returns the opposite boolean value. For example, if the first argument results in true then this function returns false instead.


1372
1373
1374
# File 'lib/oga/xpath/evaluator.rb', line 1372

def on_call_not(context, expression)
  return !on_call_boolean(context, expression)
end

#on_call_number(context, expression = nil) ⇒ Float

Evaluates the number() function call.

This function call converts its first argument or the current context node to a number, similar to the string() function.

Examples:

number("10") # => 10.0

See Also:

  • Oga::XPath::Evaluator.[[#on_call_string]

1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
# File 'lib/oga/xpath/evaluator.rb', line 1089

def on_call_number(context, expression = nil)
  convert = nil

  if expression
    exp_retval = process(expression, context)

    if exp_retval.is_a?(XML::NodeSet)
      convert = first_node_text(exp_retval)

    elsif exp_retval == true
      convert = 1.0

    elsif exp_retval == false
      convert = 0.0

    elsif exp_retval
      convert = exp_retval
    end
  else
    convert = context.first.text
  end

  return to_float(convert)
end

#on_call_position(context) ⇒ Float

Processes the position() function call. This function returns the position of the current node in the current node set.


914
915
916
917
918
# File 'lib/oga/xpath/evaluator.rb', line 914

def on_call_position(context)
  index = current_node_set.index(context.first) + 1

  return index.to_f
end

#on_call_round(context, expression) ⇒ Float

Processes the round() function call.

This function call rounds the 1st argument to the closest integer, and then returns that number as a float.


1505
1506
1507
1508
1509
# File 'lib/oga/xpath/evaluator.rb', line 1505

def on_call_round(context, expression)
  number = on_call_number(context, expression)

  return number.nan? ? number : number.round.to_f
end

#on_call_starts_with(context, haystack, needle) ⇒ TrueClass|FalseClass

Processes the starts-with() function call.

This function call returns true if the string in the 1st argument starts with the string in the 2nd argument. Node sets can also be used.

Examples:

starts-with("hello world", "hello") # => true

1150
1151
1152
1153
1154
1155
1156
# File 'lib/oga/xpath/evaluator.rb', line 1150

def on_call_starts_with(context, haystack, needle)
  haystack_str = on_call_string(context, haystack)
  needle_str   = on_call_string(context, needle)

  # https://github.com/jruby/jruby/issues/1923
  return needle_str.empty? || haystack_str.start_with?(needle_str)
end

#on_call_string(context, expression = nil) ⇒ String

Evaluates the string() function call.

This function call converts the given argument or the current context node to a string. If a node set is given then only the first node is converted to a string.

Examples:

string(10) # => "10"

1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
# File 'lib/oga/xpath/evaluator.rb', line 1057

def on_call_string(context, expression = nil)
  if expression
    convert = process(expression, context)

    if convert.is_a?(XML::NodeSet)
      convert = convert[0]
    end
  else
    convert = context.first
  end

  if convert.respond_to?(:text)
    return convert.text
  else
    return to_string(convert)
  end
end

#on_call_string_length(context, expression = nil) ⇒ Float

Processes the string-length() function.

This function returns the length of the string given in the 1st argument or the current context node. If the expression is not a string it's converted to a string using the string() function.

See Also:

  • Oga::XPath::Evaluator.[[#on_call_string]

1278
1279
1280
# File 'lib/oga/xpath/evaluator.rb', line 1278

def on_call_string_length(context, expression = nil)
  return on_call_string(context, expression).length.to_f
end

#on_call_substring(context, haystack, start, length = nil) ⇒ String

Processes the substring() function call.

This function call returns the substring of the 1st argument, starting at the position given in the 2nd argument. If the third argument is given it is used as the length for the substring, otherwise the string is consumed until the end.

XPath string indexes start from position 1, not position 0.

Examples:

Using a literal string

substring("foo", 2) # => "oo"

Using a literal string with a custom length

substring("foo", 1, 2) # => "fo"

Using a node set

substring(users/user/username, 5)

1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
# File 'lib/oga/xpath/evaluator.rb', line 1252

def on_call_substring(context, haystack, start, length = nil)
  haystack_str = on_call_string(context, haystack)
  start_index  = on_call_number(context, start).to_i - 1

  if length
    length_int = on_call_number(context, length).to_i - 1
    stop_index = start_index + length_int
  else
    stop_index = -1
  end

  return haystack_str[start_index..stop_index]
end

#on_call_substring_after(context, haystack, needle) ⇒ String

Processes the substring-after() function call.

This function call returns the substring of the 1st argument that occurs after the string given in the 2nd argument. For example:

substring-after("2014-08-25", "-")

This would return "08-25" as it occurs after the first "-".


1218
1219
1220
1221
1222
1223
1224
1225
# File 'lib/oga/xpath/evaluator.rb', line 1218

def on_call_substring_after(context, haystack, needle)
  haystack_str = on_call_string(context, haystack)
  needle_str   = on_call_string(context, needle)

  before, sep, after = haystack_str.partition(needle_str)

  return sep.empty? ? sep : after
end

#on_call_substring_before(context, haystack, needle) ⇒ String

Processes the substring-before() function call.

This function call returns the substring of the 1st argument that occurs before the string given in the 2nd argument. For example:

substring-before("2014-08-25", "-")

This would return "2014" as it occurs before the first "-".


1194
1195
1196
1197
1198
1199
1200
1201
# File 'lib/oga/xpath/evaluator.rb', line 1194

def on_call_substring_before(context, haystack, needle)
  haystack_str = on_call_string(context, haystack)
  needle_str   = on_call_string(context, needle)

  before, sep, after = haystack_str.partition(needle_str)

  return sep.empty? ? sep : before
end

#on_call_sum(context, expression) ⇒ Float

Processes the sum() function call.

This function call takes a node set, converts each node to a number and then sums the values.

As an example, take the following XML:

<root>
  <a>1</a>
  <b>2</b>
</root>

Using the expression sum(root/*) the return value would be 3.0.


1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
# File 'lib/oga/xpath/evaluator.rb', line 1448

def on_call_sum(context, expression)
  nodes = process(expression, context)
  sum   = 0.0

  unless nodes.is_a?(XML::NodeSet)
    raise TypeError, 'sum() can only operate on NodeSet instances'
  end

  nodes.each do |node|
    sum += node.text.to_f
  end

  return sum
end

#on_call_translate(context, input, find, replace) ⇒ String

Processes the translate() function call.

This function takes the string of the 1st argument and replaces all characters of the 2nd argument with those specified in the 3rd argument.

Examples:

translate("bar", "abc", "ABC") # => "BAr"

1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
# File 'lib/oga/xpath/evaluator.rb', line 1317

def on_call_translate(context, input, find, replace)
  input_str     = on_call_string(context, input)
  find_chars    = on_call_string(context, find).chars.to_a
  replace_chars = on_call_string(context, replace).chars.to_a
  replaced      = input_str

  find_chars.each_with_index do |char, index|
    replace_with = replace_chars[index] ? replace_chars[index] : ''
    replaced     = replaced.gsub(char, replace_with)
  end

  return replaced
end

#on_call_true(context) ⇒ TrueClass

Processes the true() function call.

This function simply returns the boolean true.


1384
1385
1386
# File 'lib/oga/xpath/evaluator.rb', line 1384

def on_call_true(context)
  return true
end

#on_div(ast_node, context) ⇒ Float

Processes the div operator.

This operator converts the left and right expressions to numbers and divides the left number with the right number.


706
707
708
709
710
# File 'lib/oga/xpath/evaluator.rb', line 706

def on_div(ast_node, context)
  left, right = *ast_node

  return on_call_number(context, left) / on_call_number(context, right)
end

#on_eq(ast_node, context) ⇒ TrueClass|FalseClass

Processes the = operator.

This operator evaluates the expression on the left and right and returns true if they are equal. This operator can be used to compare strings, numbers and node sets. When using node sets the text of the set is compared instead of the nodes themselves. That is, nodes with different names but the same text are considered to be equal.


773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
# File 'lib/oga/xpath/evaluator.rb', line 773

def on_eq(ast_node, context)
  left  = process(ast_node.children[0], context)
  right = process(ast_node.children[1], context)

  if left.is_a?(XML::NodeSet)
    left = first_node_text(left)
  end

  if right.is_a?(XML::NodeSet)
    right = first_node_text(right)
  end

  if left.is_a?(Numeric) and !right.is_a?(Numeric)
    right = to_float(right)
  end

  if left.is_a?(String) and !right.is_a?(String)
    right = to_string(right)
  end

  return left == right
end

#on_float(ast_node, context) ⇒ Float

Processes an (float) node.


1529
1530
1531
# File 'lib/oga/xpath/evaluator.rb', line 1529

def on_float(ast_node, context)
  return ast_node.children[0]
end

#on_gt(ast_node, context) ⇒ TrueClass|FalseClass

Processes the > operator.

This operator converts the left and right expression to a number and returns true if the first number is greater than the second number.


834
835
836
837
838
# File 'lib/oga/xpath/evaluator.rb', line 834

def on_gt(ast_node, context)
  left, right = *ast_node

  return on_call_number(context, left) > on_call_number(context, right)
end

#on_gte(ast_node, context) ⇒ TrueClass|FalseClass

Processes the >= operator.

This operator converts the left and right expression to a number and returns true if the first number is greater-than or equal to the second number.


868
869
870
871
872
# File 'lib/oga/xpath/evaluator.rb', line 868

def on_gte(ast_node, context)
  left, right = *ast_node

  return on_call_number(context, left) >= on_call_number(context, right)
end

#on_int(ast_node, context) ⇒ Float

Processes an (int) node.


1518
1519
1520
# File 'lib/oga/xpath/evaluator.rb', line 1518

def on_int(ast_node, context)
  return ast_node.children[0].to_f
end

#on_lt(ast_node, context) ⇒ TrueClass|FalseClass

Processes the < operator.

This operator converts the left and right expression to a number and returns true if the first number is lower than the second number.


818
819
820
821
822
# File 'lib/oga/xpath/evaluator.rb', line 818

def on_lt(ast_node, context)
  left, right = *ast_node

  return on_call_number(context, left) < on_call_number(context, right)
end

#on_lte(ast_node, context) ⇒ TrueClass|FalseClass

Processes the <= operator.

This operator converts the left and right expression to a number and returns true if the first number is lower-than or equal to the second number.


851
852
853
854
855
# File 'lib/oga/xpath/evaluator.rb', line 851

def on_lte(ast_node, context)
  left, right = *ast_node

  return on_call_number(context, left) <= on_call_number(context, right)
end

#on_mod(ast_node, context) ⇒ Float

Processes the mod operator.

This operator converts the left and right expressions to numbers and returns the modulo of the two numbers.


722
723
724
725
726
# File 'lib/oga/xpath/evaluator.rb', line 722

def on_mod(ast_node, context)
  left, right = *ast_node

  return on_call_number(context, left) % on_call_number(context, right)
end

#on_mul(ast_node, context) ⇒ Float

Processes the * operator.

This operator converts the left and right expressions to numbers and multiplies the left number with the right number.


738
739
740
741
742
# File 'lib/oga/xpath/evaluator.rb', line 738

def on_mul(ast_node, context)
  left, right = *ast_node

  return on_call_number(context, left) * on_call_number(context, right)
end

#on_neq(ast_node, context) ⇒ Object

Processes the != operator.

This operator does the exact opposite of the = operator. See #on_eq for more information.

See Also:

  • Oga::XPath::Evaluator.[[#on_eq]

804
805
806
# File 'lib/oga/xpath/evaluator.rb', line 804

def on_neq(ast_node, context)
  return !on_eq(ast_node, context)
end

#on_or(ast_node, context) ⇒ TrueClass|FalseClass

Processes the or operator.

This operator returns true if one of the expressions evaluates to true, otherwise false is returned. If the first expression evaluates to true the second expression is ignored.


674
675
676
677
678
# File 'lib/oga/xpath/evaluator.rb', line 674

def on_or(ast_node, context)
  left, right = *ast_node

  return on_call_boolean(context, left) || on_call_boolean(context, right)
end

#on_path(ast_node, context) ⇒ Oga::XML::NodeSet

Processes a relative XPath expression such as foo.

Paths are evaluated using a "short-circuit" mechanism similar to Ruby's && / and operator. Whenever a path results in an empty node set the evaluation is aborted immediately.


149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/oga/xpath/evaluator.rb', line 149

def on_path(ast_node, context)
  nodes = XML::NodeSet.new

  ast_node.children.each do |test|
    nodes = process(test, context)

    if nodes.empty?
      break
    else
      context = nodes
    end
  end

  return nodes
end

#on_pipe(ast_node, context) ⇒ Oga::XML::NodeSet

Processes the pipe (|) operator. This operator creates a union of two sets.


640
641
642
643
644
# File 'lib/oga/xpath/evaluator.rb', line 640

def on_pipe(ast_node, context)
  left, right = *ast_node

  return process(left, context) + process(right, context)
end

#on_predicate(ast_node, context) ⇒ Oga::XML::NodeSet

Processes a predicate.


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
# File 'lib/oga/xpath/evaluator.rb', line 189

def on_predicate(ast_node, context)
  test, predicate = *ast_node
  final_nodes     = XML::NodeSet.new

  context.each do |context_node|
    initial_nodes = process(test, XML::NodeSet.new([context_node]))
    xpath_index   = 1

    initial_nodes.each do |xml_node|
      retval = with_node_set(initial_nodes) do
        process(predicate, XML::NodeSet.new([xml_node]))
      end

      # Numeric values are used as node set indexes.
      if retval.is_a?(Numeric)
        final_nodes << xml_node if retval.to_i == xpath_index

      # Node sets, strings, booleans, etc
      elsif retval
        if retval.respond_to?(:empty?) and retval.empty?
          next
        end

        final_nodes << xml_node
      end

      xpath_index += 1
    end
  end

  return final_nodes
end

#on_string(ast_node, context) ⇒ String

Processes a (string) node.


1540
1541
1542
# File 'lib/oga/xpath/evaluator.rb', line 1540

def on_string(ast_node, context)
  return ast_node.children[0]
end

#on_sub(ast_node, context) ⇒ Float

Processes the - operator.

This operator converts the left and right expressions to numbers and subtracts the right number of the left number.


754
755
756
757
758
# File 'lib/oga/xpath/evaluator.rb', line 754

def on_sub(ast_node, context)
  left, right = *ast_node

  return on_call_number(context, left) - on_call_number(context, right)
end

#on_test(ast_node, context) ⇒ Oga::XML::NodeSet

Processes a node test.


172
173
174
175
176
177
178
179
180
# File 'lib/oga/xpath/evaluator.rb', line 172

def on_test(ast_node, context)
  nodes = XML::NodeSet.new

  context.each do |xml_node|
    nodes << xml_node if node_matches?(xml_node, ast_node)
  end

  return nodes
end

#on_type_test(ast_node, context) ⇒ Oga::XML::NodeSet

Dispatches node type matching to dedicated handlers.


553
554
555
556
557
558
559
# File 'lib/oga/xpath/evaluator.rb', line 553

def on_type_test(ast_node, context)
  name, test = *ast_node

  handler = name.gsub('-', '_')

  return send("on_type_test_#{handler}", test, context)
end

#on_type_test_comment(ast_node, context) ⇒ Oga::XML::NodeSet

Processes the comment() type test. This matches only comment nodes.


604
605
606
607
608
609
610
611
612
# File 'lib/oga/xpath/evaluator.rb', line 604

def on_type_test_comment(ast_node, context)
  nodes = XML::NodeSet.new

  context.each do |node|
    nodes << node if node.is_a?(XML::Comment)
  end

  return nodes
end

#on_type_test_node(ast_node, context) ⇒ Oga::XML::NodeSet

Processes the node type matcher. This matcher matches all node types.


568
569
570
571
572
573
574
575
576
577
578
# File 'lib/oga/xpath/evaluator.rb', line 568

def on_type_test_node(ast_node, context)
  nodes = XML::NodeSet.new

  context.each do |node|
    if node.is_a?(XML::Node) or node.is_a?(XML::Document)
      nodes << node
    end
  end

  return nodes
end

#on_type_test_processing_instruction(ast_node, context) ⇒ Oga::XML::NodeSet

Processes the processing-instruction() type test. This matches only processing-instruction nodes.


622
623
624
625
626
627
628
629
630
# File 'lib/oga/xpath/evaluator.rb', line 622

def on_type_test_processing_instruction(ast_node, context)
  nodes = XML::NodeSet.new

  context.each do |node|
    nodes << node if node.is_a?(XML::ProcessingInstruction)
  end

  return nodes
end

#on_type_test_text(ast_node, context) ⇒ Oga::XML::NodeSet

Processes the text() type test. This matches only text nodes.


587
588
589
590
591
592
593
594
595
# File 'lib/oga/xpath/evaluator.rb', line 587

def on_type_test_text(ast_node, context)
  nodes = XML::NodeSet.new

  context.each do |node|
    nodes << node if node.is_a?(XML::Text)
  end

  return nodes
end

#on_var(ast_node, context) ⇒ Mixed

Processes a variable reference. If the variable is not defined an error is raised.

Raises:

  • (RuntimeError)

1553
1554
1555
1556
1557
1558
1559
1560
1561
# File 'lib/oga/xpath/evaluator.rb', line 1553

def on_var(ast_node, context)
  name = ast_node.children[0]

  if @variables.key?(name)
    return @variables[name]
  else
    raise "Undefined XPath variable: #{name}"
  end
end

#parent_node(node) ⇒ Oga::XML::Node|Oga::XML::Document

Returns the parent node of node, or node itself if its a Document.


1781
1782
1783
# File 'lib/oga/xpath/evaluator.rb', line 1781

def parent_node(node)
  return node.respond_to?(:parent) ? node.parent : node
end

#process(ast_node, context) ⇒ Oga::XML::NodeSet

Processes an XPath node by dispatching it and the given context to a dedicated handler method. Handler methods are called "on_X" where "X" is the node type.


114
115
116
117
118
# File 'lib/oga/xpath/evaluator.rb', line 114

def process(ast_node, context)
  handler = "on_#{ast_node.type}"

  return send(handler, ast_node, context)
end

#root_node(node) ⇒ Oga::XML::Node|Oga::XML::Document

Returns the root node of node, or node itself if its a Document.


1771
1772
1773
# File 'lib/oga/xpath/evaluator.rb', line 1771

def root_node(node)
  return node.respond_to?(:root_node) ? node.root_node : node
end

#to_float(value) ⇒ Float

Converts the given value to a float. If the value can't be converted to a float NaN is returned instead.


1715
1716
1717
# File 'lib/oga/xpath/evaluator.rb', line 1715

def to_float(value)
  return Float(value) rescue Float::NAN
end

#to_string(value) ⇒ String

Converts the given value to a string according to the XPath string conversion rules.


1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
# File 'lib/oga/xpath/evaluator.rb', line 1726

def to_string(value)
  # If we have a number that has a zero decimal (e.g. 10.0) we want to
  # get rid of that decimal. For this we'll first convert the number to
  # an integer.
  if value.is_a?(Float) and value.modulo(1).zero?
    value = value.to_i
  end

  return value.to_s
end

#type_matches?(xml_node, ast_node) ⇒ TrueClass|FalseClass


1668
1669
1670
1671
1672
# File 'lib/oga/xpath/evaluator.rb', line 1668

def type_matches?(xml_node, ast_node)
  context = XML::NodeSet.new([xml_node])

  return process(ast_node, context).length > 0
end

#with_node_set(nodes) ⇒ Object

Stores the specified node set and yields the supplied block. The return value of this method is whatever the block returned.

Examples:

retval = with_node_set(context) do
  process(....)
end

1748
1749
1750
1751
1752
1753
1754
1755
1756
# File 'lib/oga/xpath/evaluator.rb', line 1748

def with_node_set(nodes)
  @node_sets << nodes

  retval = yield

  @node_sets.pop

  return retval
end