Skip to content

Commit be3de1f

Browse files
committed
XPath nodeset always in document-order
In XPath 1.0 spec, nodeset are unordered set, but many operations are required to use document ordered. Functions such as local-name should use first node in document order. Filter expressions such as `(preceding::foo)[1]` need to use document-order. JavaScript's `XPathResult.ORDERED_NODE_ITERATOR_TYPE` `XPathResult.FIRST_ORDERED_NODE_TYPE` always orders in document order. This commit will make REXML match to this ordering, and also fixes inconsistent ordering bug: same xpath may return document-ordered nodes or reverse-document-ordered nodes depending on `nodesets.size`.
1 parent a98066c commit be3de1f

3 files changed

Lines changed: 55 additions & 28 deletions

File tree

lib/rexml/xpath_parser.rb

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ def expr( path_stack, nodeset, context=nil )
262262
nodesets
263263
end
264264
when :ancestor
265-
nodeset = step(path_stack) do
265+
nodeset = step(path_stack, order: :reverse) do
266266
nodesets = []
267267
# new_nodes = {}
268268
nodeset.each do |node|
@@ -280,7 +280,7 @@ def expr( path_stack, nodeset, context=nil )
280280
nodesets
281281
end
282282
when :ancestor_or_self
283-
nodeset = step(path_stack) do
283+
nodeset = step(path_stack, order: :reverse) do
284284
nodesets = []
285285
# new_nodes = {}
286286
nodeset.each do |node|
@@ -471,7 +471,7 @@ def step(path_stack, any_type: :element, order: :forward)
471471
nodesets = evaluate_predicate(predicate_expression, nodesets)
472472
end
473473
if nodesets.size == 1
474-
ordered_nodeset = nodesets[0]
474+
ordered_nodeset = order == :forward ? nodesets.first : nodesets.first.reverse
475475
else
476476
seen = {}.compare_by_identity
477477
raw_nodes = []
@@ -483,7 +483,7 @@ def step(path_stack, any_type: :element, order: :forward)
483483
raw_nodes << raw_node
484484
end
485485
end
486-
ordered_nodeset = sort(raw_nodes, order)
486+
ordered_nodeset = sort(raw_nodes)
487487
end
488488
new_nodeset = []
489489
ordered_nodeset.each do |node|
@@ -667,7 +667,7 @@ def leave(tag, *args)
667667
# in and out of function calls. If I knew what the index of the nodes was,
668668
# I wouldn't have to do this. Maybe add a document IDX for each node?
669669
# Problems with mutable documents. Or, rewrite everything.
670-
def sort(array_of_nodes, order)
670+
def sort(array_of_nodes)
671671
new_arry = []
672672
array_of_nodes.each { |node|
673673
node_idx = []
@@ -679,11 +679,7 @@ def sort(array_of_nodes, order)
679679
new_arry << [ node_idx.reverse, node ]
680680
}
681681
ordered = new_arry.sort_by do |index, node|
682-
if order == :forward
683-
index
684-
else
685-
index.map(&:-@)
686-
end
682+
index
687683
end
688684
ordered.collect do |_index, node|
689685
node

test/xpath/test_axis_preceding_sibling.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def test_preceding_sibling_axis
2323
assert_equal "6", context.attributes["id"]
2424

2525
prev = XPath.first(context, "preceding-sibling::f")
26-
assert_equal "5", prev.attributes["id"]
26+
assert_equal "3", prev.attributes["id"]
2727

2828
prev = XPath.first(context, "preceding-sibling::f[1]")
2929
assert_equal "5", prev.attributes["id"]

test/xpath/test_base.rb

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -382,32 +382,39 @@ def test_preceding
382382
start = XPath.first( d, "/a/b[@id='1']" )
383383
assert_equal 'b', start.name
384384
c = XPath.first( start, "preceding::c" )
385-
assert_equal '2', c.attributes['id']
385+
assert_equal '0', c.attributes['id']
386386

387-
c1, c0 = XPath.match( d, "/a/b/c[@id='2']/preceding::node()" )
388-
assert_equal '1', c1.attributes['id']
387+
b0, b2, c0, c1 = XPath.match( d, "/a/b/c[@id='2']/preceding::node()" )
388+
assert_equal 'b', b0.name
389+
assert_equal 'b', b2.name
390+
assert_equal 'c', c0.name
391+
assert_equal 'c', c1.name
392+
393+
assert_equal '0', b0.attributes['id']
394+
assert_equal '2', b2.attributes['id']
389395
assert_equal '0', c0.attributes['id']
396+
assert_equal '1', c1.attributes['id']
390397

391-
c2, c1, c0, b, b2, b0 = XPath.match( start, "preceding::node()" )
398+
b0, b2, b, c0, c1, c2 = XPath.match( start, "preceding::node()" )
392399

393-
assert_equal 'c', c2.name
394-
assert_equal 'c', c1.name
395400
assert_equal 'c', c0.name
396-
assert_equal 'b', b.name
397-
assert_equal 'b', b2.name
401+
assert_equal 'c', c1.name
402+
assert_equal 'c', c2.name
398403
assert_equal 'b', b0.name
404+
assert_equal 'b', b2.name
405+
assert_equal 'b', b.name
399406

400-
assert_equal '2', c2.attributes['id']
401-
assert_equal '1', c1.attributes['id']
402407
assert_equal '0', c0.attributes['id']
403-
assert b.attributes.empty?
404-
assert_equal '2', b2.attributes['id']
408+
assert_equal '1', c1.attributes['id']
409+
assert_equal '2', c2.attributes['id']
405410
assert_equal '0', b0.attributes['id']
411+
assert_equal '2', b2.attributes['id']
412+
assert b.attributes.empty?
406413

407414
d = REXML::Document.new("<a><b/><c/><d/></a>")
408415
matches = REXML::XPath.match(d, "/a/d/preceding::node()")
409-
assert_equal("c", matches[0].name)
410-
assert_equal("b", matches[1].name)
416+
assert_equal("b", matches[0].name)
417+
assert_equal("c", matches[1].name)
411418

412419
s = "<a><b><c id='1'/></b><b><b><c id='2'/><c id='3'/></b><c id='4'/></b><c id='NOMATCH'><c id='5'/></c></a>"
413420
d = REXML::Document.new(s)
@@ -425,7 +432,7 @@ def test_preceding_multiple
425432
XML
426433
doc = REXML::Document.new(source)
427434
matches = REXML::XPath.match(doc, "a/d/preceding::*")
428-
assert_equal(["d", "c", "b"], matches.map(&:name))
435+
assert_equal(["b", "c", "d"], matches.map(&:name))
429436
end
430437

431438
def test_following_multiple
@@ -498,7 +505,7 @@ def test_preceding_sibling_across_multiple_nodes
498505
XML
499506
doc = REXML::Document.new(source)
500507
matches = REXML::XPath.match(doc, "a/b/x/preceding-sibling::*")
501-
assert_equal(["e", "d", "c"], matches.map(&:name))
508+
assert_equal(["c", "d", "e"], matches.map(&:name))
502509
end
503510

504511
def test_preceding_sibling_within_single_node
@@ -511,7 +518,7 @@ def test_preceding_sibling_within_single_node
511518
XML
512519
doc = REXML::Document.new(source)
513520
matches = REXML::XPath.match(doc, "a/b/x/preceding-sibling::*")
514-
assert_equal(["e", "x", "d", "c"], matches.map(&:name))
521+
assert_equal(["c", "d", "x", "e"], matches.map(&:name))
515522
end
516523

517524
def test_following
@@ -901,6 +908,30 @@ def test_ordering
901908
r.collect {|element| element.attribute("id").value})
902909
end
903910

911+
def test_order_consistency
912+
doc = REXML::Document.new('<a><b><c><d id="1"><d id="2"/></d></c></b></a>')
913+
assert_equal('a', REXML::XPath.first(doc, '//d/ancestor::*').name)
914+
assert_equal('a', REXML::XPath.first(doc, '//d[@id="2"]/ancestor::*').name)
915+
assert_equal(%w[a b c d], REXML::XPath.match(doc, '//d/ancestor::*').map(&:name))
916+
assert_equal(%w[a b c d], REXML::XPath.match(doc, '//d[@id="2"]/ancestor::*').map(&:name))
917+
918+
assert_equal('a', REXML::XPath.first(doc, '//d/ancestor-or-self::*').name)
919+
assert_equal('a', REXML::XPath.first(doc, '//d[@id="2"]/ancestor-or-self::*').name)
920+
assert_equal(%w[a b c d d], REXML::XPath.match(doc, '//d/ancestor-or-self::*').map(&:name))
921+
assert_equal(%w[a b c d d], REXML::XPath.match(doc, '//d[@id="2"]/ancestor-or-self::*').map(&:name))
922+
923+
doc = REXML::Document.new('<a><b/><c/><d id="1"/><d id="2"/></a>')
924+
assert_equal('b', REXML::XPath.first(doc, '//d/preceding::*').name)
925+
assert_equal('b', REXML::XPath.first(doc, '//d[@id="2"]/preceding::*').name)
926+
assert_equal(%w[b c d], REXML::XPath.match(doc, '//d/preceding::*').map(&:name))
927+
assert_equal(%w[b c d], REXML::XPath.match(doc, '//d[@id="2"]/preceding::*').map(&:name))
928+
929+
assert_equal('b', REXML::XPath.first(doc, '//d/preceding-sibling::*').name)
930+
assert_equal('b', REXML::XPath.first(doc, '//d[@id="2"]/preceding-sibling::*').name)
931+
assert_equal(%w[b c d], REXML::XPath.match(doc, '//d/preceding-sibling::*').map(&:name))
932+
assert_equal(%w[b c d], REXML::XPath.match(doc, '//d[@id="2"]/preceding-sibling::*').map(&:name))
933+
end
934+
904935
def test_descendant_or_self_ordering
905936
source = "<a>
906937
<b>

0 commit comments

Comments
 (0)