New Upstream Release - golang-github-antchfx-xpath

Ready changes

Summary

Merged new upstream version: 1.2.4 (was: 1.2.2).

Resulting package

Built on 2023-06-24T15:00 (took 4m36s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases golang-github-antchfx-xpath-dev

Lintian Result

Diff

diff --git a/README.md b/README.md
index 540285d..3435fa9 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,7 @@ Supported Features
 
 - `(a, b, c)` : Evaluates each of its operands and concatenates the resulting sequences, in order, into a single result sequence
 
+- `(a/b)` : Selects all matches nodes as grouping set.
 
 #### Node Axes 
 
@@ -138,6 +139,7 @@ Supported Features
 `lang()`| ✗ |
 `last()`| ✓ |
 `local-name()`| ✓ |
+`matches()`| ✓ |
 `name()`| ✓ |
 `namespace-uri()`| ✓ |
 `normalize-space()`| ✓ |
@@ -157,16 +159,4 @@ Supported Features
 `system-property()`| ✗ |
 `translate()`| ✓ |
 `true()`| ✓ |
-`unparsed-entity-url()` | ✗ |
-
-Changelogs
-===
-
-2019-03-19 
-- optimize XPath `|` operation performance. [#33](https://github.com/antchfx/xpath/issues/33). Tips: suggest split into multiple subquery if you have a lot of `|` operations.
-
-2019-01-29
--  improvement `normalize-space` function. [#32](https://github.com/antchfx/xpath/issues/32)
-
-2018-12-07
--  supports XPath 2.0 Sequence expressions. [#30](https://github.com/antchfx/xpath/pull/30) by [@minherz](https://github.com/minherz).
\ No newline at end of file
+`unparsed-entity-url()` | ✗ |
\ No newline at end of file
diff --git a/assert_test.go b/assert_test.go
new file mode 100644
index 0000000..166b76e
--- /dev/null
+++ b/assert_test.go
@@ -0,0 +1,51 @@
+package xpath
+
+import (
+	"reflect"
+	"testing"
+)
+
+func assertEqual(tb testing.TB, v1, v2 interface{}) {
+	if !reflect.DeepEqual(v1, v2) {
+		tb.Fatalf("'%+v' and '%+v' are not equal", v1, v2)
+	}
+}
+
+func assertNoErr(tb testing.TB, err error) {
+	if err != nil {
+		tb.Fatalf("expected no err, but got: %s", err.Error())
+	}
+}
+
+func assertErr(tb testing.TB, err error) {
+	if err == nil {
+		tb.Fatal("expected err, but got nil")
+	}
+}
+
+func assertTrue(tb testing.TB, v bool) {
+	if !v {
+		tb.Fatal("expected true, but got false")
+	}
+}
+
+func assertFalse(tb testing.TB, v bool) {
+	if v {
+		tb.Fatal("expected false, but got true")
+	}
+}
+
+func assertNil(tb testing.TB, v interface{}) {
+	if v != nil && !reflect.ValueOf(v).IsNil() {
+		tb.Fatalf("expected nil, but got: %+v", v)
+	}
+}
+
+func assertPanic(t *testing.T, f func()) {
+	defer func() {
+		if r := recover(); r == nil {
+			t.Errorf("The code did not panic")
+		}
+	}()
+	f()
+}
diff --git a/build.go b/build.go
index b7f850f..4129a21 100644
--- a/build.go
+++ b/build.go
@@ -42,8 +42,14 @@ func axisPredicate(root *axisNode) func(NodeNavigator) bool {
 	}
 	nametest := root.LocalName != "" || root.Prefix != ""
 	predicate := func(n NodeNavigator) bool {
-		if typ == n.NodeType() || typ == allNode || typ == TextNode {
+		if typ == n.NodeType() || typ == allNode {
 			if nametest {
+				type namespaceURL interface {
+					NamespaceURL() string
+				}
+				if ns, ok := n.(namespaceURL); ok && root.hasNamespaceURI {
+					return root.LocalName == n.LocalName() && root.namespaceURI == ns.NamespaceURL()
+				}
 				if root.LocalName == n.LocalName() && root.Prefix == n.Prefix() {
 					return true
 				}
@@ -88,7 +94,10 @@ func (b *builder) processAxisNode(root *axisNode) (query, error) {
 					}
 					return v
 				}
-				qyOutput = &descendantQuery{Input: qyGrandInput, Predicate: filter, Self: true}
+				// fix `//*[contains(@id,"food")]//*[contains(@id,"food")]`, see https://github.com/antchfx/htmlquery/issues/52
+				// Skip the current node(Self:false) for the next descendants nodes.
+				_, ok := qyGrandInput.(*contextQuery)
+				qyOutput = &descendantQuery{Input: qyGrandInput, Predicate: filter, Self: ok}
 				return qyOutput, nil
 			}
 		}
@@ -193,8 +202,23 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
 		if err != nil {
 			return nil, err
 		}
-
 		qyOutput = &functionQuery{Input: b.firstInput, Func: containsFunc(arg1, arg2)}
+	case "matches":
+		//matches(string , pattern)
+		if len(root.Args) != 2 {
+			return nil, errors.New("xpath: matches function must have two parameters")
+		}
+		var (
+			arg1, arg2 query
+			err        error
+		)
+		if arg1, err = b.processNode(root.Args[0]); err != nil {
+			return nil, err
+		}
+		if arg2, err = b.processNode(root.Args[1]); err != nil {
+			return nil, err
+		}
+		qyOutput = &functionQuery{Input: b.firstInput, Func: matchesFunc(arg1, arg2)}
 	case "substring":
 		//substring( string , start [, length] )
 		if len(root.Args) < 2 {
@@ -332,7 +356,15 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
 			},
 		}
 	case "last":
-		qyOutput = &functionQuery{Input: b.firstInput, Func: lastFunc}
+		switch typ := b.firstInput.(type) {
+		case *groupQuery, *filterQuery:
+			// https://github.com/antchfx/xpath/issues/76
+			// https://github.com/antchfx/xpath/issues/78
+			qyOutput = &lastQuery{Input: typ}
+		default:
+			qyOutput = &functionQuery{Input: b.firstInput, Func: lastFunc}
+		}
+
 	case "position":
 		qyOutput = &functionQuery{Input: b.firstInput, Func: positionFunc}
 	case "boolean", "number", "string":
@@ -435,13 +467,15 @@ func (b *builder) processOperatorNode(root *operatorNode) (query, error) {
 	}
 	var qyOutput query
 	switch root.Op {
-	case "+", "-", "div", "mod": // Numeric operator
+	case "+", "-", "*", "div", "mod": // Numeric operator
 		var exprFunc func(interface{}, interface{}) interface{}
 		switch root.Op {
 		case "+":
 			exprFunc = plusFunc
 		case "-":
 			exprFunc = minusFunc
+		case "*":
+			exprFunc = mulFunc
 		case "div":
 			exprFunc = divFunc
 		case "mod":
@@ -494,16 +528,24 @@ func (b *builder) processNode(root node) (q query, err error) {
 		b.firstInput = q
 	case nodeFilter:
 		q, err = b.processFilterNode(root.(*filterNode))
+		b.firstInput = q
 	case nodeFunction:
 		q, err = b.processFunctionNode(root.(*functionNode))
 	case nodeOperator:
 		q, err = b.processOperatorNode(root.(*operatorNode))
+	case nodeGroup:
+		q, err = b.processNode(root.(*groupNode).Input)
+		if err != nil {
+			return
+		}
+		q = &groupQuery{Input: q}
+		b.firstInput = q
 	}
 	return
 }
 
 // build builds a specified XPath expressions expr.
-func build(expr string) (q query, err error) {
+func build(expr string, namespaces map[string]string) (q query, err error) {
 	defer func() {
 		if e := recover(); e != nil {
 			switch x := e.(type) {
@@ -516,7 +558,7 @@ func build(expr string) (q query, err error) {
 			}
 		}
 	}()
-	root := parse(expr)
+	root := parse(expr, namespaces)
 	b := &builder{}
 	return b.processNode(root)
 }
diff --git a/cache.go b/cache.go
new file mode 100644
index 0000000..31a2b33
--- /dev/null
+++ b/cache.go
@@ -0,0 +1,80 @@
+package xpath
+
+import (
+	"regexp"
+	"sync"
+)
+
+type loadFunc func(key interface{}) (interface{}, error)
+
+const (
+	defaultCap = 65536
+)
+
+// The reason we're building a simple capacity-resetting loading cache (when capacity reached) instead of using
+// something like github.com/hashicorp/golang-lru is primarily due to (not wanting to create) external dependency.
+// Currently this library has 0 external dep (other than go sdk), and supports go 1.6, 1.9, and 1.10 (and later).
+// Creating external lib dependencies (plus their transitive dependencies) would make things hard if not impossible.
+// We expect under most circumstances, the defaultCap is big enough for any long running services that use this
+// library if their xpath regexp cardinality is low. However, in extreme cases when the capacity is reached, we
+// simply reset the cache, taking a small subsequent perf hit (next to nothing considering amortization) in trade
+// of more complex and less performant LRU type of construct.
+type loadingCache struct {
+	sync.RWMutex
+	cap   int
+	load  loadFunc
+	m     map[interface{}]interface{}
+	reset int
+}
+
+// NewLoadingCache creates a new instance of a loading cache with capacity. Capacity must be >= 0, or
+// it will panic. Capacity == 0 means the cache growth is unbounded.
+func NewLoadingCache(load loadFunc, capacity int) *loadingCache {
+	if capacity < 0 {
+		panic("capacity must be >= 0")
+	}
+	return &loadingCache{cap: capacity, load: load, m: make(map[interface{}]interface{})}
+}
+
+func (c *loadingCache) get(key interface{}) (interface{}, error) {
+	c.RLock()
+	v, found := c.m[key]
+	c.RUnlock()
+	if found {
+		return v, nil
+	}
+	v, err := c.load(key)
+	if err != nil {
+		return nil, err
+	}
+	c.Lock()
+	if c.cap > 0 && len(c.m) >= c.cap {
+		c.m = map[interface{}]interface{}{key: v}
+		c.reset++
+	} else {
+		c.m[key] = v
+	}
+	c.Unlock()
+	return v, nil
+}
+
+var (
+	// RegexpCache is a loading cache for string -> *regexp.Regexp mapping. It is exported so that in rare cases
+	// client can customize load func and/or capacity.
+	RegexpCache = defaultRegexpCache()
+)
+
+func defaultRegexpCache() *loadingCache {
+	return NewLoadingCache(
+		func(key interface{}) (interface{}, error) {
+			return regexp.Compile(key.(string))
+		}, defaultCap)
+}
+
+func getRegexp(pattern string) (*regexp.Regexp, error) {
+	exp, err := RegexpCache.get(pattern)
+	if err != nil {
+		return nil, err
+	}
+	return exp.(*regexp.Regexp), nil
+}
diff --git a/cache_test.go b/cache_test.go
new file mode 100644
index 0000000..665bcd9
--- /dev/null
+++ b/cache_test.go
@@ -0,0 +1,166 @@
+package xpath
+
+import (
+	"errors"
+	"fmt"
+	"math/rand"
+	"strconv"
+	"sync"
+	"testing"
+)
+
+func TestLoadingCache(t *testing.T) {
+	c := NewLoadingCache(
+		func(key interface{}) (interface{}, error) {
+			switch v := key.(type) {
+			case int:
+				return strconv.Itoa(v), nil
+			default:
+				return nil, errors.New("invalid type")
+			}
+		},
+		2) // cap = 2
+	assertEqual(t, 0, len(c.m))
+	v, err := c.get(1)
+	assertNoErr(t, err)
+	assertEqual(t, "1", v)
+	assertEqual(t, 1, len(c.m))
+
+	v, err = c.get(1)
+	assertNoErr(t, err)
+	assertEqual(t, "1", v)
+	assertEqual(t, 1, len(c.m))
+
+	v, err = c.get(2)
+	assertNoErr(t, err)
+	assertEqual(t, "2", v)
+	assertEqual(t, 2, len(c.m))
+
+	// over capacity, m is reset
+	v, err = c.get(3)
+	assertNoErr(t, err)
+	assertEqual(t, "3", v)
+	assertEqual(t, 1, len(c.m))
+
+	// Invalid capacity
+	assertPanic(t, func() {
+		NewLoadingCache(func(key interface{}) (interface{}, error) { return key, nil }, -1)
+	})
+
+	// Loading failure
+	c = NewLoadingCache(
+		func(key interface{}) (interface{}, error) {
+			if key.(int)%2 == 0 {
+				return key, nil
+			} else {
+				return nil, fmt.Errorf("artificial error: %d", key.(int))
+			}
+		}, 0)
+	v, err = c.get(12)
+	assertNoErr(t, err)
+	assertEqual(t, 12, v)
+	_, err = c.get(21)
+	assertErr(t, err)
+	assertEqual(t, "artificial error: 21", err.Error())
+}
+
+const (
+	benchLoadingCacheRandSeed    = 12345
+	benchLoadingCacheConcurrency = 5
+	benchLoadingCacheKeyRange    = 2000
+	benchLoadingCacheCap         = 1000
+)
+
+func BenchmarkLoadingCacheCapped_SingleThread(b *testing.B) {
+	rand.Seed(benchLoadingCacheRandSeed)
+	c := NewLoadingCache(
+		func(key interface{}) (interface{}, error) {
+			return key, nil
+		}, benchLoadingCacheCap)
+	for i := 0; i < b.N; i++ {
+		k := rand.Intn(benchLoadingCacheKeyRange)
+		v, _ := c.get(k)
+		if k != v {
+			b.FailNow()
+		}
+	}
+	b.Logf("N=%d, reset=%d", b.N, c.reset)
+}
+
+func BenchmarkLoadingCacheCapped_MultiThread(b *testing.B) {
+	rand.Seed(benchLoadingCacheRandSeed)
+	c := NewLoadingCache(
+		func(key interface{}) (interface{}, error) {
+			return key, nil
+		}, benchLoadingCacheCap)
+	wg := sync.WaitGroup{}
+	wg.Add(benchLoadingCacheConcurrency)
+	for i := 0; i < benchLoadingCacheConcurrency; i++ {
+		go func() {
+			for j := 0; j < b.N; j++ {
+				k := rand.Intn(benchLoadingCacheKeyRange)
+				v, _ := c.get(k)
+				if k != v {
+					b.FailNow()
+				}
+			}
+			defer wg.Done()
+		}()
+	}
+	wg.Wait()
+	b.Logf("N=%d, concurrency=%d, reset=%d", b.N, benchLoadingCacheConcurrency, c.reset)
+}
+
+func BenchmarkLoadingCacheNoCap_SingleThread(b *testing.B) {
+	rand.Seed(benchLoadingCacheRandSeed)
+	c := NewLoadingCache(
+		func(key interface{}) (interface{}, error) {
+			return key, nil
+		}, 0) // 0 => no cap
+	for i := 0; i < b.N; i++ {
+		k := rand.Intn(benchLoadingCacheKeyRange)
+		v, _ := c.get(k)
+		if k != v {
+			b.FailNow()
+		}
+	}
+	b.Logf("N=%d, reset=%d", b.N, c.reset)
+}
+
+func BenchmarkLoadingCacheNoCap_MultiThread(b *testing.B) {
+	rand.Seed(benchLoadingCacheRandSeed)
+	c := NewLoadingCache(
+		func(key interface{}) (interface{}, error) {
+			return key, nil
+		}, 0) // 0 => no cap
+	wg := sync.WaitGroup{}
+	wg.Add(benchLoadingCacheConcurrency)
+	for i := 0; i < benchLoadingCacheConcurrency; i++ {
+		go func() {
+			for j := 0; j < b.N; j++ {
+				k := rand.Intn(benchLoadingCacheKeyRange)
+				v, _ := c.get(k)
+				if k != v {
+					b.FailNow()
+				}
+			}
+			defer wg.Done()
+		}()
+	}
+	wg.Wait()
+	b.Logf("N=%d, concurrency=%d, reset=%d", b.N, benchLoadingCacheConcurrency, c.reset)
+}
+
+func TestGetRegexp(t *testing.T) {
+	RegexpCache = defaultRegexpCache()
+	assertEqual(t, 0, len(RegexpCache.m))
+	assertEqual(t, defaultCap, RegexpCache.cap)
+	exp, err := getRegexp("^[0-9]{3,5}$")
+	assertNoErr(t, err)
+	assertTrue(t, exp.MatchString("3141"))
+	assertFalse(t, exp.MatchString("3"))
+	exp, err = getRegexp("[invalid")
+	assertErr(t, err)
+	assertEqual(t, "error parsing regexp: missing closing ]: `[invalid`", err.Error())
+	assertNil(t, exp)
+}
diff --git a/debian/changelog b/debian/changelog
index 960eeb0..854ab0b 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,4 +1,4 @@
-golang-github-antchfx-xpath (1.1.8-1) UNRELEASED; urgency=medium
+golang-github-antchfx-xpath (1.2.4-1) UNRELEASED; urgency=medium
 
   [ Dawid Dziurla ]
   * d/copyright: delete Files-Excluded
@@ -9,8 +9,10 @@ golang-github-antchfx-xpath (1.1.8-1) UNRELEASED; urgency=medium
   [ Debian Janitor ]
   * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository,
     Repository-Browse.
+  * New upstream release.
+  * New upstream release.
 
- -- Dawid Dziurla <dawidd0811@gmail.com>  Sun, 24 May 2020 15:05:08 +0200
+ -- Dawid Dziurla <dawidd0811@gmail.com>  Sat, 24 Jun 2023 14:56:37 -0000
 
 golang-github-antchfx-xpath (1.1.2-2) unstable; urgency=medium
 
diff --git a/func.go b/func.go
index 3873e33..afe5988 100644
--- a/func.go
+++ b/func.go
@@ -4,11 +4,26 @@ import (
 	"errors"
 	"fmt"
 	"math"
-	"regexp"
 	"strconv"
 	"strings"
+	"sync"
+	"unicode"
 )
 
+// Defined an interface of stringBuilder that compatible with
+// strings.Builder(go 1.10) and bytes.Buffer(< go 1.10)
+type stringBuilder interface {
+	WriteRune(r rune) (n int, err error)
+	WriteString(s string) (int, error)
+	Reset()
+	Grow(n int)
+	String() string
+}
+
+var builderPool = sync.Pool{New: func() interface{} {
+	return newStringBuilder()
+}}
+
 // The XPath function list.
 
 func predicate(q query) func(NodeNavigator) bool {
@@ -58,6 +73,7 @@ func lastFunc(q query, t iterator) interface{} {
 // countFunc is a XPath Node Set functions count(node-set).
 func countFunc(q query, t iterator) interface{} {
 	var count = 0
+	q = functionArgs(q)
 	test := predicate(q)
 	switch typ := q.Evaluate(t).(type) {
 	case query:
@@ -73,7 +89,7 @@ func countFunc(q query, t iterator) interface{} {
 // sumFunc is a XPath Node Set functions sum(node-set).
 func sumFunc(q query, t iterator) interface{} {
 	var sum float64
-	switch typ := q.Evaluate(t).(type) {
+	switch typ := functionArgs(q).Evaluate(t).(type) {
 	case query:
 		for node := typ.Select(t); node != nil; node = typ.Select(t) {
 			if v, err := strconv.ParseFloat(node.Value(), 64); err == nil {
@@ -106,29 +122,31 @@ func asNumber(t iterator, o interface{}) float64 {
 		return typ
 	case string:
 		v, err := strconv.ParseFloat(typ, 64)
-		if err != nil {
-			panic(errors.New("ceiling() function argument type must be a node-set or number"))
+		if err == nil {
+			return v
 		}
-		return v
 	}
-	return 0
+	return math.NaN()
 }
 
 // ceilingFunc is a XPath Node Set functions ceiling(node-set).
 func ceilingFunc(q query, t iterator) interface{} {
-	val := asNumber(t, q.Evaluate(t))
+	val := asNumber(t, functionArgs(q).Evaluate(t))
+	// if math.IsNaN(val) {
+	// 	panic(errors.New("ceiling() function argument type must be a valid number"))
+	// }
 	return math.Ceil(val)
 }
 
 // floorFunc is a XPath Node Set functions floor(node-set).
 func floorFunc(q query, t iterator) interface{} {
-	val := asNumber(t, q.Evaluate(t))
+	val := asNumber(t, functionArgs(q).Evaluate(t))
 	return math.Floor(val)
 }
 
 // roundFunc is a XPath Node Set functions round(node-set).
 func roundFunc(q query, t iterator) interface{} {
-	val := asNumber(t, q.Evaluate(t))
+	val := asNumber(t, functionArgs(q).Evaluate(t))
 	//return math.Round(val)
 	return round(val)
 }
@@ -140,7 +158,7 @@ func nameFunc(arg query) func(query, iterator) interface{} {
 		if arg == nil {
 			v = t.Current()
 		} else {
-			v = arg.Select(t)
+			v = arg.Clone().Select(t)
 			if v == nil {
 				return ""
 			}
@@ -160,7 +178,7 @@ func localNameFunc(arg query) func(query, iterator) interface{} {
 		if arg == nil {
 			v = t.Current()
 		} else {
-			v = arg.Select(t)
+			v = arg.Clone().Select(t)
 			if v == nil {
 				return ""
 			}
@@ -177,7 +195,7 @@ func namespaceFunc(arg query) func(query, iterator) interface{} {
 			v = t.Current()
 		} else {
 			// Get the first node in the node-set if specified.
-			v = arg.Select(t)
+			v = arg.Clone().Select(t)
 			if v == nil {
 				return ""
 			}
@@ -201,7 +219,7 @@ func asBool(t iterator, v interface{}) bool {
 	case *NodeIterator:
 		return v.MoveNext()
 	case bool:
-		return bool(v)
+		return v
 	case float64:
 		return v != 0
 	case string:
@@ -239,19 +257,19 @@ func asString(t iterator, v interface{}) string {
 
 // booleanFunc is a XPath functions boolean([node-set]).
 func booleanFunc(q query, t iterator) interface{} {
-	v := q.Evaluate(t)
+	v := functionArgs(q).Evaluate(t)
 	return asBool(t, v)
 }
 
 // numberFunc is a XPath functions number([node-set]).
 func numberFunc(q query, t iterator) interface{} {
-	v := q.Evaluate(t)
+	v := functionArgs(q).Evaluate(t)
 	return asNumber(t, v)
 }
 
 // stringFunc is a XPath functions string([node-set]).
 func stringFunc(q query, t iterator) interface{} {
-	v := q.Evaluate(t)
+	v := functionArgs(q).Evaluate(t)
 	return asString(t, v)
 }
 
@@ -338,15 +356,39 @@ func containsFunc(arg1, arg2 query) func(query, iterator) interface{} {
 	}
 }
 
-var (
-	regnewline  = regexp.MustCompile(`[\r\n\t]`)
-	regseqspace = regexp.MustCompile(`\s{2,}`)
-)
+// matchesFunc is an XPath function that tests a given string against a regexp pattern.
+// Note: does not support https://www.w3.org/TR/xpath-functions-31/#func-matches 3rd optional `flags` argument; if
+// needed, directly put flags in the regexp pattern, such as `(?i)^pattern$` for `i` flag.
+func matchesFunc(arg1, arg2 query) func(query, iterator) interface{} {
+	return func(q query, t iterator) interface{} {
+		var s string
+		switch typ := functionArgs(arg1).Evaluate(t).(type) {
+		case string:
+			s = typ
+		case query:
+			node := typ.Select(t)
+			if node == nil {
+				return ""
+			}
+			s = node.Value()
+		}
+		var pattern string
+		var ok bool
+		if pattern, ok = functionArgs(arg2).Evaluate(t).(string); !ok {
+			panic(errors.New("matches() function second argument type must be string"))
+		}
+		re, err := getRegexp(pattern)
+		if err != nil {
+			panic(fmt.Errorf("matches() function second argument is not a valid regexp pattern, err: %s", err.Error()))
+		}
+		return re.MatchString(s)
+	}
+}
 
 // normalizespaceFunc is XPath functions normalize-space(string?)
 func normalizespaceFunc(q query, t iterator) interface{} {
 	var m string
-	switch typ := q.Evaluate(t).(type) {
+	switch typ := functionArgs(q).Evaluate(t).(type) {
 	case string:
 		m = typ
 	case query:
@@ -356,10 +398,26 @@ func normalizespaceFunc(q query, t iterator) interface{} {
 		}
 		m = node.Value()
 	}
-	m = strings.TrimSpace(m)
-	m = regnewline.ReplaceAllString(m, " ")
-	m = regseqspace.ReplaceAllString(m, " ")
-	return m
+	var b = builderPool.Get().(stringBuilder)
+	b.Grow(len(m))
+
+	runeStr := []rune(strings.TrimSpace(m))
+	l := len(runeStr)
+	for i := range runeStr {
+		r := runeStr[i]
+		isSpace := unicode.IsSpace(r)
+		if !(isSpace && (i+1 < l && unicode.IsSpace(runeStr[i+1]))) {
+			if isSpace {
+				r = ' '
+			}
+			b.WriteRune(r)
+		}
+	}
+	result := b.String()
+	b.Reset()
+	builderPool.Put(b)
+
+	return result
 }
 
 // substringFunc is XPath functions substring function returns a part of a given string.
@@ -466,7 +524,7 @@ func translateFunc(arg1, arg2, arg3 query) func(query, iterator) interface{} {
 		src := asString(t, functionArgs(arg2).Evaluate(t))
 		dst := asString(t, functionArgs(arg3).Evaluate(t))
 
-		var replace []string
+		replace := make([]string, 0, len(src))
 		for i, s := range src {
 			d := ""
 			if i < len(dst) {
@@ -491,7 +549,7 @@ func replaceFunc(arg1, arg2, arg3 query) func(query, iterator) interface{} {
 
 // notFunc is XPATH functions not(expression) function operation.
 func notFunc(q query, t iterator) interface{} {
-	switch v := q.Evaluate(t).(type) {
+	switch v := functionArgs(q).Evaluate(t).(type) {
 	case bool:
 		return !v
 	case query:
@@ -507,20 +565,25 @@ func notFunc(q query, t iterator) interface{} {
 // concat( string1 , string2 [, stringn]* )
 func concatFunc(args ...query) func(query, iterator) interface{} {
 	return func(q query, t iterator) interface{} {
-		var a []string
+		b := builderPool.Get().(stringBuilder)
 		for _, v := range args {
 			v = functionArgs(v)
+
 			switch v := v.Evaluate(t).(type) {
 			case string:
-				a = append(a, v)
+				b.WriteString(v)
 			case query:
 				node := v.Select(t)
 				if node != nil {
-					a = append(a, node.Value())
+					b.WriteString(node.Value())
 				}
 			}
 		}
-		return strings.Join(a, "")
+		result := b.String()
+		b.Reset()
+		builderPool.Put(b)
+
+		return result
 	}
 }
 
diff --git a/func_go110.go b/func_go110.go
index 500880f..d6ca451 100644
--- a/func_go110.go
+++ b/func_go110.go
@@ -2,8 +2,15 @@
 
 package xpath
 
-import "math"
+import (
+	"math"
+	"strings"
+)
 
 func round(f float64) int {
 	return int(math.Round(f))
 }
+
+func newStringBuilder() stringBuilder {
+	return &strings.Builder{}
+}
diff --git a/func_pre_go110.go b/func_pre_go110.go
index 043616b..335141f 100644
--- a/func_pre_go110.go
+++ b/func_pre_go110.go
@@ -2,7 +2,10 @@
 
 package xpath
 
-import "math"
+import (
+	"bytes"
+	"math"
+)
 
 // math.Round() is supported by Go 1.10+,
 // This method just compatible for version <1.10.
@@ -13,3 +16,7 @@ func round(f float64) int {
 	}
 	return int(f + math.Copysign(0.5, f))
 }
+
+func newStringBuilder() stringBuilder {
+	return &bytes.Buffer{}
+}
diff --git a/func_test.go b/func_test.go
new file mode 100644
index 0000000..2ee13fe
--- /dev/null
+++ b/func_test.go
@@ -0,0 +1,48 @@
+package xpath
+
+import "testing"
+
+type testQuery string
+
+func (t testQuery) Select(_ iterator) NodeNavigator {
+	panic("implement me")
+}
+
+func (t testQuery) Clone() query {
+	return t
+}
+
+func (t testQuery) Evaluate(_ iterator) interface{} {
+	return string(t)
+}
+
+const strForNormalization = "\t    \rloooooooonnnnnnngggggggg  \r \n tes  \u00a0 t strin \n\n \r g "
+const expectedStrAfterNormalization = `loooooooonnnnnnngggggggg tes t strin g`
+
+func Test_NormalizeSpaceFunc(t *testing.T) {
+	result := normalizespaceFunc(testQuery(strForNormalization), nil).(string)
+	if expectedStrAfterNormalization != result {
+		t.Fatalf("unexpected result '%s'", result)
+	}
+}
+
+func Test_ConcatFunc(t *testing.T) {
+	result := concatFunc(testQuery("a"), testQuery("b"))(nil, nil).(string)
+	if "ab" != result {
+		t.Fatalf("unexpected result '%s'", result)
+	}
+}
+
+func Benchmark_NormalizeSpaceFunc(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		_ = normalizespaceFunc(testQuery(strForNormalization), nil)
+	}
+}
+
+func Benchmark_ConcatFunc(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		_ = concatFunc(testQuery("a"), testQuery("b"))(nil, nil)
+	}
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..6745c56
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/antchfx/xpath
+
+go 1.14
diff --git a/operator.go b/operator.go
index f9c10bc..eb38ac6 100644
--- a/operator.go
+++ b/operator.go
@@ -165,15 +165,28 @@ func cmpNodeSetString(t iterator, op string, m, n interface{}) bool {
 func cmpNodeSetNodeSet(t iterator, op string, m, n interface{}) bool {
 	a := m.(query)
 	b := n.(query)
-	x := a.Select(t)
-	if x == nil {
-		return false
-	}
-	y := b.Select(t)
-	if y == nil {
-		return false
+	for {
+		x := a.Select(t)
+		if x == nil {
+			return false
+		}
+
+		y := b.Select(t)
+		if y == nil {
+			return false
+		}
+
+		for {
+			if cmpStringStringF(op, x.Value(), y.Value()) {
+				return true
+			}
+			if y = b.Select(t); y == nil {
+				break
+			}
+		}
+		// reset
+		b.Evaluate(t)
 	}
-	return cmpStringStringF(op,x.Value(),y.Value())
 }
 
 func cmpStringNumeric(t iterator, op string, m, n interface{}) bool {
diff --git a/parse.go b/parse.go
index fb9abe3..cbd289a 100644
--- a/parse.go
+++ b/parse.go
@@ -65,11 +65,13 @@ const (
 	nodeOperator
 	nodeVariable
 	nodeConstantOperand
+	nodeGroup
 )
 
 type parser struct {
-	r *scanner
-	d int
+	r          *scanner
+	d          int
+	namespaces map[string]string
 }
 
 // newOperatorNode returns new operator node OperatorNode.
@@ -83,8 +85,8 @@ func newOperandNode(v interface{}) node {
 }
 
 // newAxisNode returns new axis node AxisNode.
-func newAxisNode(axeTyp, localName, prefix, prop string, n node) node {
-	return &axisNode{
+func newAxisNode(axeTyp, localName, prefix, prop string, n node, opts ...func(p *axisNode)) node {
+	a := axisNode{
 		nodeType:  nodeAxis,
 		LocalName: localName,
 		Prefix:    prefix,
@@ -92,6 +94,10 @@ func newAxisNode(axeTyp, localName, prefix, prop string, n node) node {
 		Prop:      prop,
 		Input:     n,
 	}
+	for _, o := range opts {
+		o(&a)
+	}
+	return &a
 }
 
 // newVariableNode returns new variable node VariableNode.
@@ -104,6 +110,10 @@ func newFilterNode(n, m node) node {
 	return &filterNode{nodeType: nodeFilter, Input: n, Condition: m}
 }
 
+func newGroupNode(n node) node {
+	return &groupNode{nodeType: nodeGroup, Input: n}
+}
+
 // newRootNode returns a root node.
 func newRootNode(s string) node {
 	return &rootNode{nodeType: nodeRoot, slash: s}
@@ -464,7 +474,16 @@ func (p *parser) parseNodeTest(n node, axeTyp string) (opnd node) {
 			if p.r.name == "*" {
 				name = ""
 			}
-			opnd = newAxisNode(axeTyp, name, prefix, "", n)
+			opnd = newAxisNode(axeTyp, name, prefix, "", n, func(a *axisNode) {
+				if prefix != "" && p.namespaces != nil {
+					if ns, ok := p.namespaces[prefix]; ok {
+						a.hasNamespaceURI = true
+						a.namespaceURI = ns
+					} else {
+						panic(fmt.Sprintf("prefix %s not defined.", prefix))
+					}
+				}
+			})
 		}
 	case itemStar:
 		opnd = newAxisNode(axeTyp, "", "", "", n)
@@ -492,6 +511,9 @@ func (p *parser) parsePrimaryExpr(n node) (opnd node) {
 	case itemLParens:
 		p.next()
 		opnd = p.parseExpression(n)
+		if opnd.Type() != nodeConstantOperand {
+			opnd = newGroupNode(opnd)
+		}
 		p.skipItem(itemRParens)
 	case itemName:
 		if p.r.canBeFunc && !isNodeType(p.r) {
@@ -523,11 +545,11 @@ func (p *parser) parseMethod(n node) node {
 }
 
 // Parse parsing the XPath express string expr and returns a tree node.
-func parse(expr string) node {
+func parse(expr string, namespaces map[string]string) node {
 	r := &scanner{text: expr}
 	r.nextChar()
 	r.nextItem()
-	p := &parser{r: r}
+	p := &parser{r: r, namespaces: namespaces}
 	return p.parseExpression(nil)
 }
 
@@ -555,11 +577,13 @@ func (o *operatorNode) String() string {
 // axisNode holds a location step.
 type axisNode struct {
 	nodeType
-	Input     node
-	Prop      string // node-test name.[comment|text|processing-instruction|node]
-	AxeType   string // name of the axes.[attribute|ancestor|child|....]
-	LocalName string // local part name of node.
-	Prefix    string // prefix name of node.
+	Input           node
+	Prop            string // node-test name.[comment|text|processing-instruction|node]
+	AxeType         string // name of the axes.[attribute|ancestor|child|....]
+	LocalName       string // local part name of node.
+	Prefix          string // prefix name of node.
+	namespaceURI    string // namespace URI of node
+	hasNamespaceURI bool   // if namespace URI is set (can be "")
 }
 
 func (a *axisNode) String() string {
@@ -587,6 +611,16 @@ func (o *operandNode) String() string {
 	return fmt.Sprintf("%v", o.Val)
 }
 
+// groupNode holds a set of node expression
+type groupNode struct {
+	nodeType
+	Input node
+}
+
+func (g *groupNode) String() string {
+	return fmt.Sprintf("%s", g.Input)
+}
+
 // filterNode holds a condition filter.
 type filterNode struct {
 	nodeType
diff --git a/query.go b/query.go
index 47f8076..4e6c634 100644
--- a/query.go
+++ b/query.go
@@ -56,7 +56,7 @@ func (c *contextQuery) Evaluate(iterator) interface{} {
 }
 
 func (c *contextQuery) Clone() query {
-	return &contextQuery{count: 0, Root: c.Root}
+	return &contextQuery{Root: c.Root}
 }
 
 // ancestorQuery is an XPath ancestor node query.(ancestor::*|ancestor-self::*)
@@ -558,8 +558,8 @@ func (f *filterQuery) do(t iterator) bool {
 		pt := getNodePosition(f.Input)
 		return int(val.Float()) == pt
 	default:
-		if q, ok := f.Predicate.(query); ok {
-			return q.Select(t) != nil
+		if f.Predicate != nil {
+			return f.Predicate.Select(t) != nil
 		}
 	}
 	return false
@@ -577,7 +577,7 @@ func (f *filterQuery) Select(t iterator) NodeNavigator {
 
 		node := f.Input.Select(t)
 		if node == nil {
-			return node
+			return nil
 		}
 		node = node.Copy()
 
@@ -669,6 +669,33 @@ func (c *constantQuery) Clone() query {
 	return c
 }
 
+type groupQuery struct {
+	posit int
+
+	Input query
+}
+
+func (g *groupQuery) Select(t iterator) NodeNavigator {
+	node := g.Input.Select(t)
+	if node == nil {
+		return nil
+	}
+	g.posit++
+	return node
+}
+
+func (g *groupQuery) Evaluate(t iterator) interface{} {
+	return g.Input.Evaluate(t)
+}
+
+func (g *groupQuery) Clone() query {
+	return &groupQuery{Input: g.Input.Clone()}
+}
+
+func (g *groupQuery) position() int {
+	return g.posit
+}
+
 // logicalQuery is an XPath logical expression.
 type logicalQuery struct {
 	Left, Right query
@@ -791,6 +818,8 @@ func (b *booleanQuery) Select(t iterator) NodeNavigator {
 }
 
 func (b *booleanQuery) Evaluate(t iterator) interface{} {
+	n := t.Current().Copy()
+
 	m := b.Left.Evaluate(t)
 	left := asBool(t, m)
 	if b.IsOr && left {
@@ -798,6 +827,8 @@ func (b *booleanQuery) Evaluate(t iterator) interface{} {
 	} else if !b.IsOr && !left {
 		return false
 	}
+
+	t.Current().MoveTo(n)
 	m = b.Right.Evaluate(t)
 	return asBool(t, m)
 }
@@ -863,6 +894,35 @@ func (u *unionQuery) Clone() query {
 	return &unionQuery{Left: u.Left.Clone(), Right: u.Right.Clone()}
 }
 
+type lastQuery struct {
+	buffer  []NodeNavigator
+	counted bool
+
+	Input query
+}
+
+func (q *lastQuery) Select(t iterator) NodeNavigator {
+	return nil
+}
+
+func (q *lastQuery) Evaluate(t iterator) interface{} {
+	if !q.counted {
+		for {
+			node := q.Input.Select(t)
+			if node == nil {
+				break
+			}
+			q.buffer = append(q.buffer, node.Copy())
+		}
+		q.counted = true
+	}
+	return float64(len(q.buffer))
+}
+
+func (q *lastQuery) Clone() query {
+	return &lastQuery{Input: q.Input.Clone()}
+}
+
 func getHashCode(n NodeNavigator) uint64 {
 	var sb bytes.Buffer
 	switch n.NodeType() {
diff --git a/xpath.go b/xpath.go
index 5f6aa89..1c0a5a2 100644
--- a/xpath.go
+++ b/xpath.go
@@ -141,7 +141,7 @@ func Compile(expr string) (*Expr, error) {
 	if expr == "" {
 		return nil, errors.New("expr expression is nil")
 	}
-	qy, err := build(expr)
+	qy, err := build(expr, nil)
 	if err != nil {
 		return nil, err
 	}
@@ -159,3 +159,18 @@ func MustCompile(expr string) *Expr {
 	}
 	return exp
 }
+
+// CompileWithNS compiles an XPath expression string, using given namespaces map.
+func CompileWithNS(expr string, namespaces map[string]string) (*Expr, error) {
+	if expr == "" {
+		return nil, errors.New("expr expression is nil")
+	}
+	qy, err := build(expr, namespaces)
+	if err != nil {
+		return nil, err
+	}
+	if qy == nil {
+		return nil, fmt.Errorf(fmt.Sprintf("undeclared variable in XPath expression: %s", expr))
+	}
+	return &Expr{s: expr, q: qy}, nil
+}
diff --git a/xpath_test.go b/xpath_test.go
index 5b62cca..3ca03a4 100644
--- a/xpath_test.go
+++ b/xpath_test.go
@@ -2,6 +2,7 @@ package xpath
 
 import (
 	"bytes"
+	"fmt"
 	"strings"
 	"testing"
 )
@@ -29,6 +30,60 @@ func TestCompile(t *testing.T) {
 	if err != nil {
 		t.Fatalf("/a/b/(c, .[not(c)]) should be correct but got error %s", err)
 	}
+	_, err = Compile("/pre:foo")
+	if err != nil {
+		t.Fatalf("/pre:foo should be correct but got error %s", err)
+	}
+}
+
+func TestCompileWithNS(t *testing.T) {
+	_, err := CompileWithNS("/foo", nil)
+	if err != nil {
+		t.Fatalf("/foo {nil} should be correct but got error %s", err)
+	}
+	_, err = CompileWithNS("/foo", map[string]string{})
+	if err != nil {
+		t.Fatalf("/foo {} should be correct but got error %s", err)
+	}
+	_, err = CompileWithNS("/foo", map[string]string{"a": "b"})
+	if err != nil {
+		t.Fatalf("/foo {a:b} should be correct but got error %s", err)
+	}
+	_, err = CompileWithNS("/a:foo", map[string]string{"a": "b"})
+	if err != nil {
+		t.Fatalf("/a:foo should be correct but got error %s", err)
+	}
+	_, err = CompileWithNS("/u:foo", map[string]string{"a": "b"})
+	msg := fmt.Sprintf("%v", err)
+	if msg != "prefix u not defined." {
+		t.Fatalf("expected 'prefix u not defined' but got: %s", msg)
+	}
+}
+
+func TestNamespace(t *testing.T) {
+	doc := createNode("", RootNode)
+	books := doc.createChildNode("books", ElementNode)
+	book1 := books.createChildNode("book", ElementNode)
+	book1.createChildNode("book1", TextNode)
+	book2 := books.createChildNode("b:book", ElementNode)
+	book2.addAttribute("xmlns:b", "ns")
+	book2.createChildNode("book2", TextNode)
+	book3 := books.createChildNode("c:book", ElementNode)
+	book3.addAttribute("xmlns:c", "ns")
+	book3.createChildNode("book3", TextNode)
+
+	// Existing behaviour:
+	v := joinValues(selectNodes(doc, "//b:book"))
+	if v != "book2" {
+		t.Fatalf("expected book2 but got %s", v)
+	}
+
+	// With namespace bindings:
+	exp, _ := CompileWithNS("//x:book", map[string]string{"x": "ns"})
+	v = joinValues(iterateNodes(exp.Select(createNavigator(doc))))
+	if v != "book2,book3" {
+		t.Fatalf("expected 'book2,book3' but got %s", v)
+	}
 }
 
 func TestMustCompile(t *testing.T) {
@@ -54,6 +109,26 @@ func TestSelf(t *testing.T) {
 	testXPath2(t, html, "//body/./ul/li/a", 3)
 }
 
+func TestLastFunc(t *testing.T) {
+	testXPath3(t, html, "/head[last()]", html.FirstChild)
+	ul := selectNode(html, "//ul")
+	testXPath3(t, html, "//li[last()]", ul.LastChild)
+	list := selectNodes(html, "//li/a[last()]")
+	if got := len(list); got != 3 {
+		t.Fatalf("expected %d, but got %d", 3, got)
+	}
+	testXPath3(t, html, "(//ul/li)[last()]", ul.LastChild)
+
+	n := selectNode(html, "//meta[@name][last()]")
+	if n == nil {
+		t.Fatal("should found one, but got nil")
+	}
+	if expected, value := "description", n.getAttribute("name"); expected != value {
+		t.Fatalf("expected, %s but got %s", expected, value)
+	}
+
+}
+
 func TestParent(t *testing.T) {
 	testXPath(t, html.LastChild, "..", "html")
 	testXPath(t, html.LastChild, "parent::*", "html")
@@ -96,8 +171,8 @@ func TestChild(t *testing.T) {
 }
 
 func TestDescendant(t *testing.T) {
-	testXPath2(t, html, "descendant::*", 15)
-	testXPath2(t, html, "/head/descendant::*", 2)
+	testXPath2(t, html, "descendant::*", 16)
+	testXPath2(t, html, "/head/descendant::*", 3)
 	testXPath2(t, html, "//ul/descendant::*", 7)  // <li> + <a>
 	testXPath2(t, html, "//ul/descendant::li", 4) // <li>
 }
@@ -110,6 +185,12 @@ func TestAncestor(t *testing.T) {
 
 func TestFollowingSibling(t *testing.T) {
 	var list []*TNode
+	list = selectNodes(html2, "//h1/span/following-sibling::text()")
+	for _, n := range list {
+		if n.Type != TextNode {
+			t.Errorf("expected node is text but got:%s nodes %d", n.Data, len(list))
+		}
+	}
 	list = selectNodes(html, "//li/following-sibling::*")
 	for _, n := range list {
 		if n.Data != "li" {
@@ -162,14 +243,14 @@ func TestStarWide(t *testing.T) {
 func TestNodeTestType(t *testing.T) {
 	testXPath(t, html, "//title/text()", "Hello")
 	testXPath(t, html, "//a[@href='/']/text()", "Home")
-	testXPath2(t, html, "//head/node()", 2)
+	testXPath2(t, html, "//head/node()", 3)
 	testXPath2(t, html, "//ul/node()", 4)
 }
 
 func TestPosition(t *testing.T) {
 	testXPath3(t, html, "/head[1]", html.FirstChild) // compare to 'head' element
 	ul := selectNode(html, "//ul")
-	testXPath3(t, html, "/head[last()]", html.FirstChild)
+
 	testXPath3(t, html, "//li[1]", ul.FirstChild)
 	testXPath3(t, html, "//li[4]", ul.LastChild)
 	testXPath3(t, html, "//li[last()]", ul.LastChild)
@@ -225,8 +306,9 @@ func TestFunction(t *testing.T) {
 	testXPath(t, html, "//*[starts-with(name(),'h1')]", "h1")
 	testXPath(t, html, "//*[ends-with(name(),'itle')]", "title") // Head title
 	testXPath2(t, html, "//*[contains(@href,'a')]", 2)
-	testXPath2(t, html, "//*[starts-with(@href,'/a')]", 2) // a links: `/account`,`/about`
-	testXPath2(t, html, "//*[ends-with(@href,'t')]", 2)    // a links: `/account`,`/about`
+	testXPath2(t, html, "//*[starts-with(@href,'/a')]", 2)            // a links: `/account`,`/about`
+	testXPath2(t, html, "//*[ends-with(@href,'t')]", 2)               // a links: `/account`,`/about`
+	testXPath2(t, html, "//*[matches(@href,'(?i)^.*OU[A-Z]?T$')]", 2) // a links: `/account`,`/about`. Note use of `(?i)`
 	testXPath3(t, html, "//h1[normalize-space(text())='This is a H1']", selectNode(html, "//h1"))
 	testXPath3(t, html, "//title[substring(.,1)='Hello']", selectNode(html, "//title"))
 	testXPath3(t, html, "//title[substring(text(),1,4)='Hell']", selectNode(html, "//title"))
@@ -278,7 +360,7 @@ func TestFunction(t *testing.T) {
 
 func TestTransformFunctionReverse(t *testing.T) {
 	nodes := selectNodes(html, "reverse(//li)")
-	expectedReversedNodeValues := []string { "", "login", "about", "Home" }
+	expectedReversedNodeValues := []string{"", "login", "about", "Home"}
 	if len(nodes) != len(expectedReversedNodeValues) {
 		t.Fatalf("reverse(//li) should return %d <li> nodes", len(expectedReversedNodeValues))
 	}
@@ -309,6 +391,12 @@ func TestPanic(t *testing.T) {
 	// contains
 	assertPanic(t, func() { testXPath2(t, html, "//*[contains(0, 0)]", 0) })
 	assertPanic(t, func() { testXPath2(t, html, "//*[contains(@href, 0)]", 0) })
+	// matches
+	assertPanic(t, func() { testXPath2(t, html, "//*[matches()]", 0) })                   // arg len check failure
+	assertPanic(t, func() { testXPath2(t, html, "//*[matches(substring(), 0)]", 0) })     // first arg processing failure
+	assertPanic(t, func() { testXPath2(t, html, "//*[matches(@href, substring())]", 0) }) // second arg processing failure
+	assertPanic(t, func() { testXPath2(t, html, "//*[matches(@href, 0)]", 0) })           // second arg not string
+	assertPanic(t, func() { testXPath2(t, html, "//*[matches(@href, '[invalid')]", 0) })  // second arg invalid regexp
 	// sum
 	assertPanic(t, func() { testXPath3(t, html, "//title[sum('Hello') = 0]", nil) })
 	// substring
@@ -319,15 +407,6 @@ func TestPanic(t *testing.T) {
 
 }
 
-func assertPanic(t *testing.T, f func()) {
-	defer func() {
-		if r := recover(); r == nil {
-			t.Errorf("The code did not panic")
-		}
-	}()
-	f()
-}
-
 func TestEvaluate(t *testing.T) {
 	testEval(t, html, "count(//ul/li)", float64(4))
 	testEval(t, html, "//html/@lang", []string{"en"})
@@ -399,6 +478,16 @@ func testXPath3(t *testing.T, root *TNode, expr string, expected *TNode) {
 	}
 }
 
+func testXPath4(t *testing.T, root *TNode, expr string, expected string) {
+	node := selectNode(root, expr)
+	if node == nil {
+		t.Fatalf("`%s` returns node is nil", expr)
+	}
+	if got := node.Value(); got != expected {
+		t.Fatalf("`%s` expected \n%s,but got\n%s", expr, expected, got)
+	}
+}
+
 func iterateNavs(t *NodeIterator) []*TNodeNavigator {
 	var nodes []*TNodeNavigator
 	for t.MoveNext() {
@@ -430,6 +519,14 @@ func selectNodes(root *TNode, expr string) []*TNode {
 	return iterateNodes(t)
 }
 
+func joinValues(nodes []*TNode) string {
+	s := make([]string, 0)
+	for _, n := range nodes {
+		s = append(s, n.Value())
+	}
+	return strings.Join(s, ",")
+}
+
 func createNavigator(n *TNode) *TNodeNavigator {
 	return &TNodeNavigator{curr: n, root: n, attr: -1}
 }
@@ -482,10 +579,28 @@ func (n *TNodeNavigator) LocalName() string {
 	if n.attr != -1 {
 		return n.curr.Attr[n.attr].Key
 	}
-	return n.curr.Data
+	name := n.curr.Data
+	if strings.Contains(name, ":") {
+		return strings.Split(name, ":")[1]
+	}
+	return name
 }
 
 func (n *TNodeNavigator) Prefix() string {
+	if n.attr == -1 && strings.Contains(n.curr.Data, ":") {
+		return strings.Split(n.curr.Data, ":")[0]
+	}
+	return ""
+}
+
+func (n *TNodeNavigator) NamespaceURL() string {
+	if n.Prefix() != "" {
+		for _, a := range n.curr.Attr {
+			if a.Key == "xmlns:"+n.Prefix() {
+				return a.Value
+			}
+		}
+	}
 	return ""
 }
 
@@ -622,6 +737,14 @@ func (n *TNode) addAttribute(k, v string) {
 	n.Attr = append(n.Attr, Attribute{k, v})
 }
 
+func (n *TNode) getAttribute(key string) string {
+	for i := 0; i < len(n.Attr); i++ {
+		if n.Attr[i].Key == key {
+			return n.Attr[i].Value
+		}
+	}
+	return ""
+}
 func example2() *TNode {
 	/*
 		<html lang="en">
@@ -630,7 +753,7 @@ func example2() *TNode {
 			   <meta name="language" content="en"/>
 		   </head>
 		   <body>
-				<h1> This is a H1 </h1>
+				<h1><span>SPAN</span><a>Anchor</a> This is a H1 </h1>
 				<table>
 					<tbody>
 						<tr>
@@ -666,8 +789,12 @@ func example2() *TNode {
 	n.addAttribute("content", "en")
 	// The HTML body section.
 	body := xhtml.createChildNode("body", ElementNode)
-	n = body.createChildNode("h1", ElementNode)
-	n = n.createChildNode(" This is a H1 ", TextNode)
+	h1 := body.createChildNode("h1", ElementNode)
+	n = h1.createChildNode("span", ElementNode)
+	n = n.createChildNode("SPAN", TextNode)
+	n = h1.createChildNode("a", ElementNode)
+	n = n.createChildNode("Anchor", TextNode)
+	h1.createChildNode(" This is a H1 ", TextNode)
 
 	n = body.createChildNode("table", ElementNode)
 	tbody := n.createChildNode("tbody", ElementNode)
@@ -693,6 +820,7 @@ func example() *TNode {
 		   <head>
 			   <title>Hello</title>
 			   <meta name="language" content="en"/>
+			   <meta name="description" content="some description"/>
 		   </head>
 		   <body>
 				<h1>
@@ -722,6 +850,9 @@ func example() *TNode {
 	n = head.createChildNode("meta", ElementNode)
 	n.addAttribute("name", "language")
 	n.addAttribute("content", "en")
+	n = head.createChildNode("meta", ElementNode)
+	n.addAttribute("name", "description")
+	n.addAttribute("content", "some description")
 	// The HTML body section.
 	body := xhtml.createChildNode("body", ElementNode)
 	n = body.createChildNode("h1", ElementNode)

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/gocode/src/github.com/antchfx/xpath/assert_test.go
-rw-r--r--  root/root   /usr/share/gocode/src/github.com/antchfx/xpath/cache.go
-rw-r--r--  root/root   /usr/share/gocode/src/github.com/antchfx/xpath/cache_test.go
-rw-r--r--  root/root   /usr/share/gocode/src/github.com/antchfx/xpath/func_test.go
-rw-r--r--  root/root   /usr/share/gocode/src/github.com/antchfx/xpath/go.mod

No differences were encountered in the control files

More details

Full run details