Skip to content

Commit 3ddbdfc

Browse files
committed
xpath abbreviate: rewrite to support complex cases
GitHub: fix GH-98 Reported by pulver. Thanks!!!
1 parent 0eddba8 commit 3ddbdfc

File tree

2 files changed

+150
-39
lines changed

2 files changed

+150
-39
lines changed

lib/rexml/parsers/xpathparser.rb

Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# frozen_string_literal: false
2+
23
require_relative '../namespace'
34
require_relative '../xmltokens'
45

@@ -44,60 +45,87 @@ def abbreviate(path_or_parsed)
4445
else
4546
parsed = path_or_parsed
4647
end
47-
path = ""
48-
document = false
48+
components = []
49+
component = nil
50+
previous_op = nil
4951
while parsed.size > 0
5052
op = parsed.shift
5153
case op
5254
when :node
55+
component << "node()"
5356
when :attribute
54-
path << "/" if path.size > 0
55-
path << "@"
57+
component = "@"
58+
components << component
5659
when :child
57-
path << "/" if path.size > 0
60+
component = ""
61+
components << component
5862
when :descendant_or_self
59-
path << "//"
63+
next_op = parsed[0]
64+
if next_op == :node
65+
parsed.shift
66+
component = ""
67+
components << component
68+
else
69+
component = "descendant-or-self::"
70+
components << component
71+
end
6072
when :self
61-
path << "/"
73+
next_op = parsed[0]
74+
if next_op == :node
75+
parsed.shift
76+
components << "."
77+
else
78+
component = "self::"
79+
components << component
80+
end
6281
when :parent
63-
path << "/.."
82+
next_op = parsed[0]
83+
if next_op == :node
84+
parsed.shift
85+
components << ".."
86+
else
87+
component = "parent::"
88+
components << component
89+
end
6490
when :any
65-
path << "*"
91+
component << "*"
6692
when :text
67-
path << "text()"
93+
component << "text()"
6894
when :following, :following_sibling,
6995
:ancestor, :ancestor_or_self, :descendant,
7096
:namespace, :preceding, :preceding_sibling
71-
path << "/" unless path.size == 0
72-
path << op.to_s.tr("_", "-")
73-
path << "::"
97+
component = op.to_s.tr("_", "-") << "::"
98+
components << component
7499
when :qname
75100
prefix = parsed.shift
76101
name = parsed.shift
77-
path << prefix+":" if prefix.size > 0
78-
path << name
102+
component << prefix+":" if prefix.size > 0
103+
component << name
79104
when :predicate
80-
path << '['
81-
path << predicate_to_path( parsed.shift ) {|x| abbreviate( x ) }
82-
path << ']'
105+
component << '['
106+
component << predicate_to_path(parsed.shift) {|x| abbreviate(x)}
107+
component << ']'
83108
when :document
84-
document = true
109+
components << ""
85110
when :function
86-
path << parsed.shift
87-
path << "( "
88-
path << predicate_to_path( parsed.shift[0] ) {|x| abbreviate( x )}
89-
path << " )"
111+
component << parsed.shift
112+
component << "( "
113+
component << predicate_to_path(parsed.shift[0]) {|x| abbreviate(x)}
114+
component << " )"
90115
when :literal
91-
path << %Q{ "#{parsed.shift}" }
116+
component << quote_literal(parsed.shift)
92117
else
93-
path << "/" unless path.size == 0
94-
path << "UNKNOWN("
95-
path << op.inspect
96-
path << ")"
118+
component << "UNKNOWN("
119+
component << op.inspect
120+
component << ")"
97121
end
122+
previous_op = op
123+
end
124+
if components == [""]
125+
"/"
126+
else
127+
components.join("/")
98128
end
99-
path = "/"+path if document
100-
path
101129
end
102130

103131
def expand(path_or_parsed)
@@ -133,7 +161,6 @@ def expand(path_or_parsed)
133161
when :document
134162
document = true
135163
else
136-
path << "/" unless path.size == 0
137164
path << "UNKNOWN("
138165
path << op.inspect
139166
path << ")"
@@ -166,32 +193,26 @@ def predicate_to_path(parsed, &block)
166193
end
167194
left = predicate_to_path( parsed.shift, &block )
168195
right = predicate_to_path( parsed.shift, &block )
169-
path << " "
170196
path << left
171197
path << " "
172198
path << op.to_s
173199
path << " "
174200
path << right
175-
path << " "
176201
when :function
177202
parsed.shift
178203
name = parsed.shift
179204
path << name
180-
path << "( "
205+
path << "("
181206
parsed.shift.each_with_index do |argument, i|
182207
path << ", " if i > 0
183208
path << predicate_to_path(argument, &block)
184209
end
185-
path << " )"
210+
path << ")"
186211
when :literal
187212
parsed.shift
188-
path << " "
189213
path << quote_literal(parsed.shift)
190-
path << " "
191214
else
192-
path << " "
193215
path << yield( parsed )
194-
path << " "
195216
end
196217
return path.squeeze(" ")
197218
end

test/parser/test_xpath.rb

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,96 @@ def test_document
1515
assert_equal("/",
1616
abbreviate("/"))
1717
end
18+
19+
def test_descendant_or_self_absolute
20+
assert_equal("//a/b",
21+
abbreviate("/descendant-or-self::node()/a/b"))
22+
end
23+
24+
def test_descendant_or_self_relative
25+
assert_equal("a//b",
26+
abbreviate("a/descendant-or-self::node()/b"))
27+
end
28+
29+
def test_descendant_or_self_not_node
30+
assert_equal("/descendant-or-self::text()",
31+
abbreviate("/descendant-or-self::text()"))
32+
end
33+
34+
def test_self_absolute
35+
assert_equal("/a/./b",
36+
abbreviate("/a/self::node()/b"))
37+
end
38+
39+
def test_self_relative
40+
assert_equal("a/./b",
41+
abbreviate("a/self::node()/b"))
42+
end
43+
44+
def test_self_not_node
45+
assert_equal("/self::text()",
46+
abbreviate("/self::text()"))
47+
end
48+
49+
def test_parent_absolute
50+
assert_equal("/a/../b",
51+
abbreviate("/a/parent::node()/b"))
52+
end
53+
54+
def test_parent_relative
55+
assert_equal("a/../b",
56+
abbreviate("a/parent::node()/b"))
57+
end
58+
59+
def test_parent_not_node
60+
assert_equal("/a/parent::text()",
61+
abbreviate("/a/parent::text()"))
62+
end
63+
64+
def test_any_absolute
65+
assert_equal("/*/a",
66+
abbreviate("/*/a"))
67+
end
68+
69+
def test_any_relative
70+
assert_equal("a/*/b",
71+
abbreviate("a/*/b"))
72+
end
73+
74+
def test_following_sibling_absolute
75+
assert_equal("/following-sibling::a/b",
76+
abbreviate("/following-sibling::a/b"))
77+
end
78+
79+
def test_following_sibling_relative
80+
assert_equal("a/following-sibling::b/c",
81+
abbreviate("a/following-sibling::b/c"))
82+
end
83+
84+
def test_predicate_index
85+
assert_equal("a[5]/b",
86+
abbreviate("a[5]/b"))
87+
end
88+
89+
def test_attribute_relative
90+
assert_equal("a/@b",
91+
abbreviate("a/attribute::b"))
92+
end
93+
94+
def test_filter_attribute
95+
assert_equal("a/b[@i = 1]/c",
96+
abbreviate("a/b[attribute::i=1]/c"))
97+
end
98+
99+
def test_filter_string_single_quote
100+
assert_equal("a/b[@name = \"single ' quote\"]/c",
101+
abbreviate("a/b[attribute::name=\"single ' quote\"]/c"))
102+
end
103+
104+
def test_filter_string_double_quote
105+
assert_equal("a/b[@name = 'double \" quote']/c",
106+
abbreviate("a/b[attribute::name='double \" quote']/c"))
107+
end
18108
end
19109
end
20110
end

0 commit comments

Comments
 (0)