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