As I mentioned in https://github.com/antchfx/xpath/issues/44, I need reverse()
function which is part of xpath 2.0.
I did a quick prototyping. I'd like to have main contributors (@zhengchun ?) take a look to see if I'm completely off or not, before I create a pull request:
Below is the prototype diff (Not many tests added yet). Note the diff is large-ish because I renamed the existing functionQuery
to scalarFunctionQuery
to differentiate it from my newly added selectFunctionQuery
(which currently only has reverse
function but I think we can merge unionQuery
and logicalQuery
into selectFunctionQuery
eventually, tell me if I'm crazy)
diff --git a/build.go b/build.go
index be1214c..d6ff928 100644
--- a/build.go
+++ b/build.go
@@ -173,7 +173,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
if err != nil {
return nil, err
}
- qyOutput = &functionQuery{Input: b.firstInput, Func: startwithFunc(arg1, arg2)}
+ qyOutput = &scalarFunctionQuery{Input: b.firstInput, Func: startwithFunc(arg1, arg2)}
case "ends-with":
arg1, err := b.processNode(root.Args[0])
if err != nil {
@@ -183,7 +183,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
if err != nil {
return nil, err
}
- qyOutput = &functionQuery{Input: b.firstInput, Func: endwithFunc(arg1, arg2)}
+ qyOutput = &scalarFunctionQuery{Input: b.firstInput, Func: endwithFunc(arg1, arg2)}
case "contains":
arg1, err := b.processNode(root.Args[0])
if err != nil {
@@ -194,7 +194,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
return nil, err
}
- qyOutput = &functionQuery{Input: b.firstInput, Func: containsFunc(arg1, arg2)}
+ qyOutput = &scalarFunctionQuery{Input: b.firstInput, Func: containsFunc(arg1, arg2)}
case "substring":
//substring( string , start [, length] )
if len(root.Args) < 2 {
@@ -215,7 +215,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
return nil, err
}
}
- qyOutput = &functionQuery{Input: b.firstInput, Func: substringFunc(arg1, arg2, arg3)}
+ qyOutput = &scalarFunctionQuery{Input: b.firstInput, Func: substringFunc(arg1, arg2, arg3)}
case "substring-before", "substring-after":
//substring-xxxx( haystack, needle )
if len(root.Args) != 2 {
@@ -231,7 +231,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
if arg2, err = b.processNode(root.Args[1]); err != nil {
return nil, err
}
- qyOutput = &functionQuery{
+ qyOutput = &scalarFunctionQuery{
Input: b.firstInput,
Func: substringIndFunc(arg1, arg2, root.FuncName == "substring-after"),
}
@@ -244,7 +244,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
if err != nil {
return nil, err
}
- qyOutput = &functionQuery{Input: b.firstInput, Func: stringLengthFunc(arg1)}
+ qyOutput = &scalarFunctionQuery{Input: b.firstInput, Func: stringLengthFunc(arg1)}
case "normalize-space":
if len(root.Args) == 0 {
return nil, errors.New("xpath: normalize-space function must have at least one parameter")
@@ -253,7 +253,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
if err != nil {
return nil, err
}
- qyOutput = &functionQuery{Input: argQuery, Func: normalizespaceFunc}
+ qyOutput = &scalarFunctionQuery{Input: argQuery, Func: normalizespaceFunc}
case "replace":
//replace( string , string, string )
if len(root.Args) != 3 {
@@ -272,7 +272,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
if arg3, err = b.processNode(root.Args[2]); err != nil {
return nil, err
}
- qyOutput = &functionQuery{Input: b.firstInput, Func: replaceFunc(arg1, arg2, arg3)}
+ qyOutput = &scalarFunctionQuery{Input: b.firstInput, Func: replaceFunc(arg1, arg2, arg3)}
case "translate":
//translate( string , string, string )
if len(root.Args) != 3 {
@@ -291,7 +291,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
if arg3, err = b.processNode(root.Args[2]); err != nil {
return nil, err
}
- qyOutput = &functionQuery{Input: b.firstInput, Func: translateFunc(arg1, arg2, arg3)}
+ qyOutput = &scalarFunctionQuery{Input: b.firstInput, Func: translateFunc(arg1, arg2, arg3)}
case "not":
if len(root.Args) == 0 {
return nil, errors.New("xpath: not function must have at least one parameter")
@@ -300,7 +300,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
if err != nil {
return nil, err
}
- qyOutput = &functionQuery{Input: argQuery, Func: notFunc}
+ qyOutput = &scalarFunctionQuery{Input: argQuery, Func: notFunc}
case "name", "local-name", "namespace-uri":
if len(root.Args) > 1 {
return nil, fmt.Errorf("xpath: %s function must have at most one parameter", root.FuncName)
@@ -317,24 +317,24 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
}
switch root.FuncName {
case "name":
- qyOutput = &functionQuery{Input: b.firstInput, Func: nameFunc(arg)}
+ qyOutput = &scalarFunctionQuery{Input: b.firstInput, Func: nameFunc(arg)}
case "local-name":
- qyOutput = &functionQuery{Input: b.firstInput, Func: localNameFunc(arg)}
+ qyOutput = &scalarFunctionQuery{Input: b.firstInput, Func: localNameFunc(arg)}
case "namespace-uri":
- qyOutput = &functionQuery{Input: b.firstInput, Func: namespaceFunc(arg)}
+ qyOutput = &scalarFunctionQuery{Input: b.firstInput, Func: namespaceFunc(arg)}
}
case "true", "false":
val := root.FuncName == "true"
- qyOutput = &functionQuery{
+ qyOutput = &scalarFunctionQuery{
Input: b.firstInput,
Func: func(_ query, _ iterator) interface{} {
return val
},
}
case "last":
- qyOutput = &functionQuery{Input: b.firstInput, Func: lastFunc}
+ qyOutput = &scalarFunctionQuery{Input: b.firstInput, Func: lastFunc}
case "position":
- qyOutput = &functionQuery{Input: b.firstInput, Func: positionFunc}
+ qyOutput = &scalarFunctionQuery{Input: b.firstInput, Func: positionFunc}
case "boolean", "number", "string":
inp := b.firstInput
if len(root.Args) > 1 {
@@ -347,7 +347,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
}
inp = argQuery
}
- f := &functionQuery{Input: inp}
+ f := &scalarFunctionQuery{Input: inp}
switch root.FuncName {
case "boolean":
f.Func = booleanFunc
@@ -368,7 +368,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
if err != nil {
return nil, err
}
- qyOutput = &functionQuery{Input: argQuery, Func: countFunc}
+ qyOutput = &scalarFunctionQuery{Input: argQuery, Func: countFunc}
case "sum":
if len(root.Args) == 0 {
return nil, fmt.Errorf("xpath: sum(node-sets) function must with have parameters node-sets")
@@ -377,7 +377,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
if err != nil {
return nil, err
}
- qyOutput = &functionQuery{Input: argQuery, Func: sumFunc}
+ qyOutput = &scalarFunctionQuery{Input: argQuery, Func: sumFunc}
case "ceiling", "floor", "round":
if len(root.Args) == 0 {
return nil, fmt.Errorf("xpath: ceiling(node-sets) function must with have parameters node-sets")
@@ -386,7 +386,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
if err != nil {
return nil, err
}
- f := &functionQuery{Input: argQuery}
+ f := &scalarFunctionQuery{Input: argQuery}
switch root.FuncName {
case "ceiling":
f.Func = ceilingFunc
@@ -408,7 +408,16 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
}
args = append(args, q)
}
- qyOutput = &functionQuery{Input: b.firstInput, Func: concatFunc(args...)}
+ qyOutput = &scalarFunctionQuery{Input: b.firstInput, Func: concatFunc(args...)}
+ case "reverse":
+ if len(root.Args) == 0 {
+ return nil, fmt.Errorf("xpath: reverse(node-sets) function must with have parameters node-sets")
+ }
+ argQuery, err := b.processNode(root.Args[0])
+ if err != nil {
+ return nil, err
+ }
+ qyOutput = &selectFunctionQuery{Input: argQuery, Func: reverseFunc}
default:
return nil, fmt.Errorf("not yet support this function %s()", root.FuncName)
}
@@ -464,7 +473,7 @@ func (b *builder) processOperatorNode(root *operatorNode) (query, error) {
qyOutput = &booleanQuery{Left: left, Right: right, IsOr: isOr}
case "|":
qyOutput = &unionQuery{Left: left, Right: right}
- }
+ }
return qyOutput, nil
}
diff --git a/func.go b/func.go
index 1edeb1d..12d0ec2 100644
--- a/func.go
+++ b/func.go
@@ -526,8 +526,28 @@ func concatFunc(args ...query) func(query, iterator) interface{} {
// https://github.com/antchfx/xpath/issues/43
func functionArgs(q query) query {
- if _, ok := q.(*functionQuery); ok {
+ if _, ok := q.(*scalarFunctionQuery); ok {
return q
}
return q.Clone()
}
+
+func reverseFunc(q query, t iterator) func() NodeNavigator {
+ var list []NodeNavigator
+ for {
+ node := q.Select(t)
+ if node == nil {
+ break
+ }
+ list = append(list, node.Copy())
+ }
+ i := len(list) - 1
+ return func() NodeNavigator {
+ if i < 0 {
+ return nil
+ }
+ node := list[i]
+ i--
+ return node
+ }
+}
diff --git a/query.go b/query.go
index afeb890..115fdc8 100644
--- a/query.go
+++ b/query.go
@@ -590,25 +590,48 @@ func (f *filterQuery) Clone() query {
return &filterQuery{Input: f.Input.Clone(), Predicate: f.Predicate.Clone()}
}
-// functionQuery is an XPath function that call a function to returns
+// scalarFunctionQuery is an XPath function that call a function to returns
// value of current NodeNavigator node.
-type functionQuery struct {
+type scalarFunctionQuery struct {
Input query // Node Set
Func func(query, iterator) interface{} // The xpath function.
}
-func (f *functionQuery) Select(t iterator) NodeNavigator {
+func (f *scalarFunctionQuery) Select(t iterator) NodeNavigator {
return nil
}
// Evaluate call a specified function that will returns the
// following value type: number,string,boolean.
-func (f *functionQuery) Evaluate(t iterator) interface{} {
+func (f *scalarFunctionQuery) Evaluate(t iterator) interface{} {
return f.Func(f.Input, t)
}
-func (f *functionQuery) Clone() query {
- return &functionQuery{Input: f.Input.Clone(), Func: f.Func}
+func (f *scalarFunctionQuery) Clone() query {
+ return &scalarFunctionQuery{Input: f.Input.Clone(), Func: f.Func}
+}
+
+type selectFunctionQuery struct {
+ Input query
+ Func func(query, iterator) func() NodeNavigator
+ iterator func() NodeNavigator
+}
+
+func (f *selectFunctionQuery) Select(t iterator) NodeNavigator {
+ if f.iterator == nil {
+ f.iterator = f.Func(f.Input, t)
+ }
+ return f.iterator()
+}
+
+func (f *selectFunctionQuery) Evaluate(t iterator) interface{} {
+ // QUESTION: when is this ever called?
+ f.iterator = f.Func(f.Input, t)
+ return f
+}
+
+func (f *selectFunctionQuery) Clone() query {
+ return &selectFunctionQuery{Input: f.Input.Clone(), Func: f.Func}
}
// constantQuery is an XPath constant operand.
diff --git a/xpath_test.go b/xpath_test.go
index 7b7d14b..edb222d 100644
--- a/xpath_test.go
+++ b/xpath_test.go
@@ -1,9 +1,9 @@
package xpath
import (
- "bytes"
- "strings"
- "testing"
+ "bytes"
+ "strings"
+ "testing"
)
var (
@@ -729,3 +729,12 @@ func example() *TNode {
return xhtml
}
+
+func TestReverse(t *testing.T) {
+ list := selectNodes(html, "reverse(//li)")
+ if len(list) != 4 { t.Fatal("reverse(//li) should return 4 <li> nodes") }
+ if list[0].Value() != "" { t.Fatal("reverse(//li)[0].Value() should be ''") }
+ if list[1].Value() != "login" { t.Fatal("reverse(//li)[0].Value() should be 'login'") }
+ if list[2].Value() != "about" { t.Fatal("reverse(//li)[0].Value() should be 'about'") }
+ if list[3].Value() != "Home" { t.Fatal("reverse(//li)[0].Value() should be 'Home'") }
+}
Thank you very much!