hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

truncate.go (3785B)

    1 // Copyright 2016 The Hugo Authors. All rights reserved.
    2 //
    3 // Licensed under the Apache License, Version 2.0 (the "License");
    4 // you may not use this file except in compliance with the License.
    5 // You may obtain a copy of the License at
    6 // http://www.apache.org/licenses/LICENSE-2.0
    7 //
    8 // Unless required by applicable law or agreed to in writing, software
    9 // distributed under the License is distributed on an "AS IS" BASIS,
   10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   11 // See the License for the specific language governing permissions and
   12 // limitations under the License.
   13 
   14 package strings
   15 
   16 import (
   17 	"errors"
   18 	"html"
   19 	"html/template"
   20 	"regexp"
   21 	"unicode"
   22 	"unicode/utf8"
   23 
   24 	"github.com/spf13/cast"
   25 )
   26 
   27 var (
   28 	tagRE        = regexp.MustCompile(`^<(/)?([^ ]+?)(?:(\s*/)| .*?)?>`)
   29 	htmlSinglets = map[string]bool{
   30 		"br": true, "col": true, "link": true,
   31 		"base": true, "img": true, "param": true,
   32 		"area": true, "hr": true, "input": true,
   33 	}
   34 )
   35 
   36 type htmlTag struct {
   37 	name    string
   38 	pos     int
   39 	openTag bool
   40 }
   41 
   42 // Truncate truncates a given string to the specified length.
   43 func (ns *Namespace) Truncate(a any, options ...any) (template.HTML, error) {
   44 	length, err := cast.ToIntE(a)
   45 	if err != nil {
   46 		return "", err
   47 	}
   48 	var textParam any
   49 	var ellipsis string
   50 
   51 	switch len(options) {
   52 	case 0:
   53 		return "", errors.New("truncate requires a length and a string")
   54 	case 1:
   55 		textParam = options[0]
   56 		ellipsis = " …"
   57 	case 2:
   58 		textParam = options[1]
   59 		ellipsis, err = cast.ToStringE(options[0])
   60 		if err != nil {
   61 			return "", errors.New("ellipsis must be a string")
   62 		}
   63 		if _, ok := options[0].(template.HTML); !ok {
   64 			ellipsis = html.EscapeString(ellipsis)
   65 		}
   66 	default:
   67 		return "", errors.New("too many arguments passed to truncate")
   68 	}
   69 	if err != nil {
   70 		return "", errors.New("text to truncate must be a string")
   71 	}
   72 	text, err := cast.ToStringE(textParam)
   73 	if err != nil {
   74 		return "", errors.New("text must be a string")
   75 	}
   76 
   77 	_, isHTML := textParam.(template.HTML)
   78 
   79 	if utf8.RuneCountInString(text) <= length {
   80 		if isHTML {
   81 			return template.HTML(text), nil
   82 		}
   83 		return template.HTML(html.EscapeString(text)), nil
   84 	}
   85 
   86 	tags := []htmlTag{}
   87 	var lastWordIndex, lastNonSpace, currentLen, endTextPos, nextTag int
   88 
   89 	for i, r := range text {
   90 		if i < nextTag {
   91 			continue
   92 		}
   93 
   94 		if isHTML {
   95 			// Make sure we keep tag of HTML tags
   96 			slice := text[i:]
   97 			m := tagRE.FindStringSubmatchIndex(slice)
   98 			if len(m) > 0 && m[0] == 0 {
   99 				nextTag = i + m[1]
  100 				tagname := slice[m[4]:m[5]]
  101 				lastWordIndex = lastNonSpace
  102 				_, singlet := htmlSinglets[tagname]
  103 				if !singlet && m[6] == -1 {
  104 					tags = append(tags, htmlTag{name: tagname, pos: i, openTag: m[2] == -1})
  105 				}
  106 
  107 				continue
  108 			}
  109 		}
  110 
  111 		currentLen++
  112 		if unicode.IsSpace(r) {
  113 			lastWordIndex = lastNonSpace
  114 		} else if unicode.In(r, unicode.Han, unicode.Hangul, unicode.Hiragana, unicode.Katakana) {
  115 			lastWordIndex = i
  116 		} else {
  117 			lastNonSpace = i + utf8.RuneLen(r)
  118 		}
  119 
  120 		if currentLen > length {
  121 			if lastWordIndex == 0 {
  122 				endTextPos = i
  123 			} else {
  124 				endTextPos = lastWordIndex
  125 			}
  126 			out := text[0:endTextPos]
  127 			if isHTML {
  128 				out += ellipsis
  129 				// Close out any open HTML tags
  130 				var currentTag *htmlTag
  131 				for i := len(tags) - 1; i >= 0; i-- {
  132 					tag := tags[i]
  133 					if tag.pos >= endTextPos || currentTag != nil {
  134 						if currentTag != nil && currentTag.name == tag.name {
  135 							currentTag = nil
  136 						}
  137 						continue
  138 					}
  139 
  140 					if tag.openTag {
  141 						out += ("</" + tag.name + ">")
  142 					} else {
  143 						currentTag = &tag
  144 					}
  145 				}
  146 
  147 				return template.HTML(out), nil
  148 			}
  149 			return template.HTML(html.EscapeString(out) + ellipsis), nil
  150 		}
  151 	}
  152 
  153 	if isHTML {
  154 		return template.HTML(text), nil
  155 	}
  156 	return template.HTML(html.EscapeString(text)), nil
  157 }