Replace (path) nodes with nested nodes

This changes the XPath AST so that every segment in a path (e.g.
foo/bar) is parsed as a child node of the node that precedes it. For
example, take the following expression:

    foo/bar

This used to be parsed into the following AST:

    (path
      (axis "child" (test nil "foo"))
      (axis "child" (test nil "bar")))

This is now parsed into the following AST:

    (axis "child"
      (test nil "foo")
      (axis "child"
        (test nil "bar")))

This new AST is much easier to deal with in the XPath::Compiler class,
especially when trying to ensure that each segment operates on the
correct input.

This commit also fixes parsing of type tests with predicates, such as:

    comment()[10]

This used to throw a parser error.
This commit is contained in:
Yorick Peterse 2015-08-26 10:16:48 +02:00
parent 866044f94f
commit 365a9e9fa9
7 changed files with 160 additions and 86 deletions

View File

@ -97,7 +97,7 @@ union_expr_follow
;
expression_member
= relative_path
= path_step_or_axis
| absolute_path
| string
| number
@ -105,22 +105,16 @@ expression_member
| T_LPAREN expression T_RPAREN { val[1] }
;
# A, A/B, etc
relative_path
= path_steps { val[0].length > 1 ? s(:path, *val[0]) : val[0][0] }
;
path_steps
= path_step_or_axis path_steps_follow* { [val[0], *val[1]] }
;
path_steps_follow
= T_SLASH path_step_or_axis { val[1] }
;
# /A, /A/B, etc
absolute_path
= T_SLASH path_steps? { s(:absolute_path, *val[1]) }
= T_SLASH path_step_or_axis?
{
if val[1]
s(:absolute_path, val[1])
else
s(:absolute_path)
end
}
;
path_step_or_axis
@ -135,6 +129,7 @@ path_step
type = val[1][0]
args = val[1][1]
pred = val[1][2]
more = val[1][3]
if type.equal?(:test)
# Whenever a bare test is used (e.g. just "A") this actually means
@ -152,15 +147,38 @@ path_step
node = s(:predicate, node, pred)
end
if more
node = node.updated(nil, node.children + [more])
end
node
}
| type_test predicate? path_step_more?
{
pred = val[1]
more = val[2]
node = s(:axis, 'child', val[0])
if pred
node = s(:predicate, node, pred)
end
if more
node = node.updated(nil, node.children + [more])
end
node
}
| type_test { s(:axis, 'child', val[0]) }
;
path_step_follow
= T_LPAREN call_args T_RPAREN { [:call, val[1]] }
| T_COLON T_IDENT predicate? { [:test, val[1], val[2]] }
| predicate? { [:test, nil, val[0]] }
= T_LPAREN call_args T_RPAREN { [:call, val[1]] }
| T_COLON T_IDENT predicate? path_step_more? { [:test, val[1], val[2], val[3]] }
| predicate? path_step_more? { [:test, nil, val[0], val[1]] }
;
path_step_more
= T_SLASH path_step_or_axis { val[1] }
;
predicate
@ -194,14 +212,19 @@ call_args_follow
# child::foo, descendant-or-self::foo, etc
axis
= T_AXIS axis_value predicate?
= T_AXIS axis_value predicate? path_step_more?
{
ret = s(:axis, val[0], val[1])
ret = s(:axis, val[0], val[1])
more = val[3]
if val[2]
ret = s(:predicate, ret, val[2])
end
if more
ret = ret.updated(nil, ret.children + [more])
end
ret
}
;

View File

@ -112,8 +112,12 @@ describe Oga::XPath::Parser do
it 'parses the // axis' do
parse_xpath('//A').should == s(
:absolute_path,
s(:axis, 'descendant-or-self', s(:type_test, 'node')),
s(:axis, 'child', s(:test, nil, 'A'))
s(
:axis,
'descendant-or-self',
s(:type_test, 'node'),
s(:axis, 'child', s(:test, nil, 'A'))
),
)
end

View File

@ -24,32 +24,25 @@ describe Oga::XPath::Parser do
end
it 'parses a relative path with a function call' do
parse_xpath('foo/bar()').should == s(
:path,
s(:axis, 'child', s(:test, nil, 'foo')),
s(:call, 'bar')
)
parse_xpath('foo/bar()').should ==
s(:axis, 'child', s(:test, nil, 'foo'), s(:call, 'bar'))
end
it 'parses an absolute path with a function call' do
parse_xpath('/foo/bar()').should == s(
:absolute_path,
s(:axis, 'child', s(:test, nil, 'foo')),
s(:call, 'bar')
s(:axis, 'child', s(:test, nil, 'foo'), s(:call, 'bar'))
)
end
it 'parses a predicate followed by a function call' do
parse_xpath('div[@class="foo"]/bar()').should == s(
:path,
:predicate,
s(:axis, 'child', s(:test, nil, 'div')),
s(
:predicate,
s(:axis, 'child', s(:test, nil, 'div')),
s(
:eq,
s(:axis, 'attribute', s(:test, nil, 'class')),
s(:string, 'foo')
)
:eq,
s(:axis, 'attribute', s(:test, nil, 'class')),
s(:string, 'foo')
),
s(:call, 'bar')
)
@ -57,18 +50,15 @@ describe Oga::XPath::Parser do
it 'parses two predicates followed by a function call' do
parse_xpath('A[@x]/B[@x]/bar()').should == s(
:path,
s(
:predicate,
s(:axis, 'child', s(:test, nil, 'A')),
s(:axis, 'attribute', s(:test, nil, 'x'))
),
:predicate,
s(:axis, 'child', s(:test, nil, 'A')),
s(:axis, 'attribute', s(:test, nil, 'x')),
s(
:predicate,
s(:axis, 'child', s(:test, nil, 'B')),
s(:axis, 'attribute', s(:test, nil, 'x'))
),
s(:call, 'bar')
s(:axis, 'attribute', s(:test, nil, 'x')),
s(:call, 'bar')
)
)
end

View File

@ -1,23 +0,0 @@
require 'spec_helper'
describe Oga::XPath::Parser do
describe 'node types' do
it 'parses the "node" type' do
parse_xpath('node()').should == s(:axis, 'child', s(:type_test, 'node'))
end
it 'parses the "comment" type' do
parse_xpath('comment()')
.should == s(:axis, 'child', s(:type_test, 'comment'))
end
it 'parses the "text" type' do
parse_xpath('text()').should == s(:axis, 'child', s(:type_test, 'text'))
end
it 'parses the "processing-instruction" type' do
parse_xpath('processing-instruction()')
.should == s(:axis, 'child', s(:type_test, 'processing-instruction'))
end
end
end

View File

@ -14,13 +14,15 @@ describe Oga::XPath::Parser do
parse_xpath('A/B | C/D').should == s(
:pipe,
s(
:path,
s(:axis, 'child', s(:test, nil, 'A')),
:axis,
'child',
s(:test, nil, 'A'),
s(:axis, 'child', s(:test, nil, 'B'))
),
s(
:path,
s(:axis, 'child', s(:test, nil, 'C')),
:axis,
'child',
s(:test, nil, 'C'),
s(:axis, 'child', s(:test, nil, 'D'))
)
)

View File

@ -15,35 +15,53 @@ describe Oga::XPath::Parser do
it 'parses a relative path using two steps' do
parse_xpath('A/B').should == s(
:path,
s(:axis, 'child', s(:test, nil, 'A')),
s(:axis, 'child', s(:test, nil, 'B')),
:axis,
'child',
s(:test, nil, 'A'),
s(:axis, 'child', s(:test, nil, 'B'))
)
end
it 'parses a relative path using three steps' do
parse_xpath('A/B/C').should == s(
:path,
s(:axis, 'child', s(:test, nil, 'A')),
s(:axis, 'child', s(:test, nil, 'B')),
s(:axis, 'child', s(:test, nil, 'C')),
:axis,
'child',
s(:test, nil, 'A'),
s(
:axis,
'child',
s(:test, nil, 'B'),
s(:axis, 'child', s(:test, nil, 'C'))
)
)
end
it 'parses an expression using two paths' do
parse_xpath('/A/B').should == s(
:absolute_path,
s(:axis, 'child', s(:test, nil, 'A')),
s(:axis, 'child', s(:test, nil, 'B'))
s(
:axis,
'child',
s(:test, nil, 'A'),
s(:axis, 'child', s(:test, nil, 'B'))
)
)
end
it 'parses an expression using three paths' do
parse_xpath('/A/B/C').should == s(
:absolute_path,
s(:axis, 'child', s(:test, nil, 'A')),
s(:axis, 'child', s(:test, nil, 'B')),
s(:axis, 'child', s(:test, nil, 'C'))
s(
:axis,
'child',
s(:test, nil, 'A'),
s(
:axis,
'child',
s(:test, nil, 'B'),
s(:axis, 'child', s(:test, nil, 'C'))
)
)
)
end

View File

@ -0,0 +1,60 @@
require 'spec_helper'
describe Oga::XPath::Parser do
describe 'node() type test' do
it 'parses the standalone type test' do
parse_xpath('node()').should == s(:axis, 'child', s(:type_test, 'node'))
end
it 'parses the type test in a predicate' do
parse_xpath('node()[1]').should == s(
:predicate,
s(:axis, 'child', s(:type_test, 'node')),
s(:int, 1)
)
end
end
describe 'comment() type test' do
it 'parses the standalone type test' do
parse_xpath('comment()').should == s(:axis, 'child', s(:type_test, 'comment'))
end
it 'parses the type test in a predicate' do
parse_xpath('comment()[1]').should == s(
:predicate,
s(:axis, 'child', s(:type_test, 'comment')),
s(:int, 1)
)
end
end
describe 'text() type test' do
it 'parses the standalone type test' do
parse_xpath('text()').should == s(:axis, 'child', s(:type_test, 'text'))
end
it 'parses the type test in a predicate' do
parse_xpath('text()[1]').should == s(
:predicate,
s(:axis, 'child', s(:type_test, 'text')),
s(:int, 1)
)
end
end
describe 'processing-instruction() type test' do
it 'parses the standalone type test' do
parse_xpath('processing-instruction()').should ==
s(:axis, 'child', s(:type_test, 'processing-instruction'))
end
it 'parses the type test in a predicate' do
parse_xpath('processing-instruction()[1]').should == s(
:predicate,
s(:axis, 'child', s(:type_test, 'processing-instruction')),
s(:int, 1)
)
end
end
end