shithub: hugo

Download patch

ref: 64789fb5dcf8326f14f13d69a2576ae3aa2bbbaa
parent: 32d4bf68da7d16302f138dde343c70f9667933c4
author: Cameron Moore <moorereason@gmail.com>
date: Fri Nov 27 18:43:01 EST 2020

tpl: Refactor and fix substr logic

Fix miscalculations when start is negative.  Results should now match
PHP substr.

Fixes #7993

--- a/tpl/strings/strings.go
+++ b/tpl/strings/strings.go
@@ -279,64 +279,73 @@
 // if length is given and is negative, then that many characters will be omitted from
 // the end of string.
 func (ns *Namespace) Substr(a interface{}, nums ...interface{}) (string, error) {
-	aStr, err := cast.ToStringE(a)
+	s, err := cast.ToStringE(a)
 	if err != nil {
 		return "", err
 	}
 
+	asRunes := []rune(s)
+	rlen := len(asRunes)
+
 	var start, length int
 
-	asRunes := []rune(aStr)
-
 	switch len(nums) {
 	case 0:
-		return "", errors.New("too less arguments")
+		return "", errors.New("too few arguments")
 	case 1:
 		if start, err = cast.ToIntE(nums[0]); err != nil {
-			return "", errors.New("start argument must be integer")
+			return "", errors.New("start argument must be an integer")
 		}
-		length = len(asRunes)
+		length = rlen
 	case 2:
 		if start, err = cast.ToIntE(nums[0]); err != nil {
-			return "", errors.New("start argument must be integer")
+			return "", errors.New("start argument must be an integer")
 		}
 		if length, err = cast.ToIntE(nums[1]); err != nil {
-			return "", errors.New("length argument must be integer")
+			return "", errors.New("length argument must be an integer")
 		}
 	default:
 		return "", errors.New("too many arguments")
 	}
 
-	if start < -len(asRunes) {
+	if rlen == 0 {
+		return "", nil
+	}
+
+	if start < 0 {
+		start += rlen
+	}
+
+	// start was originally negative beyond rlen
+	if start < 0 {
 		start = 0
 	}
-	if start > len(asRunes) {
-		return "", fmt.Errorf("start position out of bounds for %d-byte string", len(aStr))
+
+	if start > rlen-1 {
+		return "", fmt.Errorf("start position out of bounds for %d-byte string", rlen)
 	}
 
-	var s, e int
-	if start >= 0 && length >= 0 {
-		s = start
-		e = start + length
-	} else if start < 0 && length >= 0 {
-		s = len(asRunes) + start - length + 1
-		e = len(asRunes) + start + 1
-	} else if start >= 0 && length < 0 {
-		s = start
-		e = len(asRunes) + length
-	} else {
-		s = len(asRunes) + start
-		e = len(asRunes) + length
+	end := rlen
+
+	if length < 0 {
+		end += length
+	} else if length > 0 {
+		end = start + length
 	}
 
-	if s > e {
-		return "", fmt.Errorf("calculated start position greater than end position: %d > %d", s, e)
+	if start >= end {
+		return "", fmt.Errorf("calculated start position greater than end position: %d > %d", start, end)
 	}
-	if e > len(asRunes) {
-		e = len(asRunes)
+
+	if end < 0 {
+		return "", nil
 	}
 
-	return string(asRunes[s:e]), nil
+	if end > rlen {
+		end = rlen
+	}
+
+	return string(asRunes[start:end]), nil
 }
 
 // Title returns a copy of the input s with all Unicode letters that begin words
--- a/tpl/strings/strings_test.go
+++ b/tpl/strings/strings_test.go
@@ -441,8 +441,11 @@
 	}{
 		{"abc", 1, 2, "bc"},
 		{"abc", 0, 1, "a"},
-		{"abcdef", -1, 2, "ef"},
-		{"abcdef", -3, 3, "bcd"},
+		{"abcdef", -1, 2, "f"},
+		{"abcdef", -3, 3, "def"},
+		{"abcdef", -1, nil, "f"},
+		{"abcdef", -2, nil, "ef"},
+		{"abcdef", -3, 1, "d"},
 		{"abcdef", 0, -1, "abcde"},
 		{"abcdef", 2, -1, "cde"},
 		{"abcdef", 4, -4, false},
@@ -480,12 +483,12 @@
 		}
 
 		if b, ok := test.expect.(bool); ok && !b {
-			c.Assert(err, qt.Not(qt.IsNil))
+			c.Assert(err, qt.Not(qt.IsNil), qt.Commentf("%v", test))
 			continue
 		}
 
 		c.Assert(err, qt.IsNil)
-		c.Assert(result, qt.Equals, test.expect)
+		c.Assert(result, qt.Equals, test.expect, qt.Commentf("%v", test))
 	}
 
 	_, err = ns.Substr("abcdef")