github.com/whatlly/hugo@v0.47.1/tpl/strings/truncate.go (about)

     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 interface{}, options ...interface{}) (template.HTML, error) {
    44  	length, err := cast.ToIntE(a)
    45  	if err != nil {
    46  		return "", err
    47  	}
    48  	var textParam interface{}
    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  }