Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions func.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"sync"
"unicode"
"unicode/utf8"
)

// Defined an interface of stringBuilder that compatible with
Expand Down Expand Up @@ -506,16 +507,18 @@ func substringFunc(arg1, arg2, arg3 query) func(query, iterator) interface{} {
if start, ok = functionArgs(arg2).Evaluate(t).(float64); !ok {
panic(errors.New("substring() function first argument type must be number"))
}
// positions are in characters, not bytes (REC 4.2)
rs := []rune(m)
// fix https://github.com/antchfx/xpath/issues/109
start = math.Round(start)
if start > float64(len(m)) {
if start > float64(len(rs)) {
return ""
}
if arg3 == nil {
if start <= 0 {
return m
}
return m[int(start)-1:]
return string(rs[int(start)-1:])
}

if length, ok = functionArgs(arg3).Evaluate(t).(float64); !ok {
Expand All @@ -524,11 +527,11 @@ func substringFunc(arg1, arg2, arg3 query) func(query, iterator) interface{} {
length = math.Round(length)
// keep positions p with start <= p < start+length, clipped to the string (REC 4.2)
first := math.Max(start, 1)
last := math.Min(start+length, float64(len(m))+1)
last := math.Min(start+length, float64(len(rs))+1)
if !(last > first) {
return ""
}
return m[int(first)-1 : int(last)-1]
return string(rs[int(first)-1 : int(last)-1])
}
}

Expand Down Expand Up @@ -578,13 +581,13 @@ func stringLengthFunc(arg1 query) func(query, iterator) interface{} {
return func(_ query, t iterator) interface{} {
switch v := functionArgs(arg1).Evaluate(t).(type) {
case string:
return float64(len(v))
return float64(utf8.RuneCountInString(v))
case query:
node := v.Select(t)
if node == nil {
break
}
return float64(len(node.Value()))
return float64(utf8.RuneCountInString(node.Value()))
}
return float64(0)
}
Expand All @@ -597,11 +600,13 @@ func translateFunc(arg1, arg2, arg3 query) func(query, iterator) interface{} {
src := asString(t, functionArgs(arg2).Evaluate(t))
dst := asString(t, functionArgs(arg3).Evaluate(t))

replace := make([]string, 0, len(src))
for i, s := range src {
// src and dst are paired by character position, not byte offset
dstRunes := []rune(dst)
replace := make([]string, 0, 2*utf8.RuneCountInString(src))
for i, s := range []rune(src) {
d := ""
if i < len(dst) {
d = string(dst[i])
if i < len(dstRunes) {
d = string(dstRunes[i])
}
replace = append(replace, string(s), d)
}
Expand Down
11 changes: 11 additions & 0 deletions xpath_function_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ func Test_func_string_length(t *testing.T) {
test_xpath_eval(t, html_example, `string-length(//title/text())`, float64(len("My page")))
test_xpath_eval(t, html_example, `string-length(//html/@lang)`, float64(len("en")))
test_xpath_count(t, employee_example, `//employee[string-length(@id) > 0]`, 3) // = //employee[@id]
// characters, not bytes (REC 4.2)
test_xpath_eval(t, empty_example, `string-length("héllo")`, float64(5))
test_xpath_eval(t, empty_example, `string-length("日本語")`, float64(3))
}

func Test_func_substring(t *testing.T) {
Expand All @@ -189,6 +192,11 @@ func Test_func_substring(t *testing.T) {
test_xpath_eval(t, empty_example, `substring("12345", 2, 10)`, "2345")
test_xpath_eval(t, empty_example, `substring("12345", 5, 10)`, "5")
test_xpath_eval(t, empty_example, `substring("motor car", 6, 20)`, " car")
// positions count characters, not bytes
test_xpath_eval(t, empty_example, `substring("héllo", 1, 3)`, "hél")
test_xpath_eval(t, empty_example, `substring("日本語abc", 2, 2)`, "本語")
test_xpath_eval(t, empty_example, `substring("héllo", 2)`, "éllo")
test_xpath_eval(t, empty_example, `substring("héllo", 1, string-length("héllo"))`, "héllo")
test_xpath_eval(t, empty_example, `substring("12345", -3, 6)`, "12")
test_xpath_eval(t, empty_example, `substring("12345", -1, 7)`, "12345")
test_xpath_eval(t, empty_example, `substring("12345", 1, number("abc"))`, "")
Expand Down Expand Up @@ -231,6 +239,9 @@ func Test_func_translate(t *testing.T) {
test_xpath_eval(t, empty_example, `translate("--aaa--","abc-","ABC")`, "AAA")
test_xpath_eval(t, empty_example, `translate("abcdabc", "abc", "AB")`, "ABdAB")
test_xpath_eval(t, empty_example, `translate('The quick brown fox', 'brown', 'red')`, "The quick red fdx")
// src and dst pair by character position
test_xpath_eval(t, empty_example, `translate("abc", "ab", "áé")`, "áéc")
test_xpath_eval(t, empty_example, `translate("日本語", "日語", "ab")`, "a本b")
}

func Test_func_matches(t *testing.T) {
Expand Down
Loading