41

In golang, is there a way to truncate text in an html template?

For example, I have the following in my template:

{{ range .SomeContent }}
 ....
    {{ .Content }}
 ....

{{ end }

{{ .Content }} produces: Interdum et malesuada fames ac ante ipsum primis in faucibus. Aliquam tempus sem ipsum, vel accumsan felis vulputate id. Donec ultricies sem purus, non aliquam orci dignissim et. Integer vitae mi arcu. Pellentesque a ipsum quis velit venenatis vulputate vulputate ut enim.

I would like to reduce that to 25 characters.

yumaikas
  • 979
  • 13
  • 24
webbydevy
  • 1,200
  • 3
  • 15
  • 21
  • Have you tried something like this? http://stackoverflow.com/questions/10200178/call-a-method-from-a-go-template – Uraza May 05 '14 at 06:57

6 Answers6

66

You can use printf in templates, which acts as fmt.Sprintf. In your case truncating a string would be as easy as:

{{ printf "%.25s" .Content }}

You can also pass the number as a separate integer argument to printf:

{{ printf "%.*s" 25 .Content }}

Note from the documentation:

Width and precision are measured in units of Unicode code points, that is, runes. (This differs from C's printf where the units are always measured in bytes.)

dolmen
  • 8,126
  • 5
  • 40
  • 42
Ondrej Slinták
  • 31,386
  • 20
  • 94
  • 126
15

You can use slice from the documentation. The below sample must work:

{{ slice .Content  0 25}}

slice returns the result of slicing its first argument by the remaining arguments. Thus "slice x 1 2" is, in Go syntax, x[1:2], while "slice x" is x[:], "slice x 1" is x[1:], and "slice x 1 2 3" is x[1:2:3]. The first argument must be a string, slice, or array.

Beware it doesn't handle index out of range issues and multi-byte strings — in case .Content is a string.

Note that slice is only available from Go 1.13. If the program that embeds text/template has been compiled using an older version of Go, use sprintf.

dolmen
  • 8,126
  • 5
  • 40
  • 42
freemanpolys
  • 1,848
  • 20
  • 19
  • 1
    Very simple and clear. I would just make sure to check the length so you don't get out of bounds. – billy Apr 18 '20 at 15:05
9

Update: Now the code below is unicode compliant for those who are working with international programs.

One thing to note is that bytes.Runes("string") below is an O(N) operation, as is the converstion from runes to a string, so this code loops over the string twice. It is likely to be more efficient to do the code below for PreviewContent()

func (c ContentHolder) PreviewContent() string {
    var numRunes = 0
    for index, _ := range c.Content {
         numRunes++
         if numRunes > 25 {
              return c.Content[:index]
         }
    }
    return c.Content
}

You have a couple options for where this function can go. Assuming that you have some type of content holder, the below can be used:

type ContentHolder struct {
    Content string
    //other fields here
}

func (c ContentHolder) PreviewContent() string {
    // This cast is O(N)
    runes := bytes.Runes([]byte(c.Content))
    if len(runes) > 25 {
         return string(runes[:25])
    }
    return string(runes)
}

Then your template will look like this:

{{ range .SomeContent }}
....
{{ .PreviewContent }}
....
{{ end }}

The other option is to create a function that will take then first 25 characters of a string. The code for that looks like this (revision of code by @Martin DrLík, link to code)

package main
import (
    "html/template"
    "log"
    "os"
)

func main() {

    funcMap := template.FuncMap{

        // Now unicode compliant
        "truncate": func(s string) string {
             var numRunes = 0
             for index, _ := range s {
                 numRunes++
                 if numRunes > 25 {
                      return s[:index]
                 }
            }
            return s
       },
    }

    const templateText = `
    Start of text
    {{ range .}}
    Entry: {{.}}
    Truncated entry: {{truncate .}}
    {{end}}
    End of Text
    `
    infoForTemplate := []string{
        "Stackoverflow is incredibly awesome",
        "Lorem ipsum dolor imet",
        "Some more example text to prove a point about truncation",
        "ПриветМирПриветМирПриветМирПриветМирПриветМирПриветМир",
    }

    tmpl, err := template.New("").Funcs(funcMap).Parse(templateText)
    if err != nil {
        log.Fatalf("parsing: %s", err)
    }

    err = tmpl.Execute(os.Stdout, infoForTemplate)
    if err != nil {
        log.Fatalf("execution: %s", err)
    }

}

This outputs:

Start of text

Entry: Stackoverflow is incredibly awesome
Truncated entry: Stackoverflow is incredib

Entry: Lorem ipsum dolor imet
Truncated entry: Lorem ipsum dolor imet

Entry: Some more example text to prove a point about truncation
Truncated entry: Some more example text to

Entry: ПриветМирПриветМирПриветМирПриветМирПриветМирПриветМир
Truncated entry: ПриветМирПриветМирПриветМ

End of Text
yumaikas
  • 979
  • 13
  • 24
4

Needs more magic for Unicode strings

This is not correct, see below

import "unicode/utf8"

func Short( s string, i int) string {
    if len( s ) < i {
        return s
    }
    if utf8.ValidString( s[:i] ) {
        return s[:i]
    }
    // The omission.
    // In reality, a rune can have 1-4 bytes width (not 1 or 2)
    return s[:i+1] // or i-1
}

But i above is not the number of chars. It's the number of bytes. Link to this code on play.golang.org

I hope this helps.


Edit

Updated: check string length. See @geoff comment below

See that answer, and play here. It's another solution.

package main

import "fmt"

func Short( s string, i int ) string {
    runes := []rune( s )
    if len( runes ) > i {
        return string( runes[:i] )
    }
    return s
}

func main() {
    fmt.Println( Short( "Hello World", 5 ) )
    fmt.Println( Short( "Привет Мир", 5 ) )
}

But if you are interested in the length in bytes:

func truncateStrings(s string, n int) string {
    if len(s) <= n {
        return s
    }
    for !utf8.ValidString(s[:n]) {
        n--
    }
    return s[:n]
}

play.golang.org. This function never panics (if n >= 0), but you can obtain an empty string play.golang.org


Also, keep in mind this experimental package golang.org/x/exp/utf8string

Package utf8string provides an efficient way to index strings by rune rather than by byte.

Community
  • 1
  • 1
Ivan Black
  • 4,827
  • 1
  • 35
  • 33
  • 1
    Note that in your last example you can get a panic if you try to truncate a string to a length longer than the string. I modified your Short method a bit to fix it: `func Short( s string, i int ) string { var runes = []rune( s ) if len(runes) > i { return string( runes[:i] ) } return s }` – Geoff Oct 16 '14 at 15:36
1

There are a lot of good answers, but sometimes it's more user-friendly to truncate without cutting words. Hugo offers template function for it. But it's difficult to use outside of Hugo, so I've implemented it:

func TruncateByWords(s string, maxWords int) string {
    processedWords := 0
    wordStarted := false
    for i := 0; i < len(s); {
        r, width := utf8.DecodeRuneInString(s[i:])
        if !unicode.IsSpace(r) {
            i += width
            wordStarted = true
            continue
        }

        if !wordStarted {
            i += width
            continue
        }

        wordStarted = false
        processedWords++
        if processedWords == maxWords {
            const ending = "..."
            if (i + len(ending)) >= len(s) {
                // Source string ending is shorter than "..."
                return s
            }

            return s[:i] + ending
        }

        i += width
    }

    // Source string contains less words count than maxWords.
    return s
}
    

And here is a test for this function:

func TestTruncateByWords(t *testing.T) {
    cases := []struct {
        in, out string
        n       int
    }{
        {"a bcde", "a...", 1},
        {"a b", "a b", 2},
        {"a b", "a b", 3},

        {"a b c", "a b c", 2},
        {"a b cd", "a b cd", 2},
        {"a b cde", "a b...", 2},

        {"  a   b    ", "  a   b...", 2},

        {"AB09C_D EFGH", "AB09C_D...", 1},
        {"Привет Гоферам", "Привет...", 1},
        {"Here are unicode spaces", "Here are...", 2},
    }

    for i, c := range cases {
        got := TruncateByWords(c.in, c.n)
        if got != c.out {
            t.Fatalf("#%d: %q != %q", i, got, c.out)
        }
    }
}
Denis Isaev
  • 141
  • 2
  • 6
-2
str := "xxxx"
n := 2
if len(str) > n {
    fmt.Println(str[:n])
}

lest say we need quarter of ascii string

str[:len(str)/4]
xsor
  • 1,074
  • 11
  • 11
  • The question is about the language implemented by package [text/template](https://pkg.go.dev/text/template). – dolmen Jun 01 '22 at 14:38