github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/helpers/general.go (about)

     1  // Copyright 2019 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 helpers
    15  
    16  import (
    17  	"bytes"
    18  	"crypto/md5"
    19  	"encoding/hex"
    20  	"fmt"
    21  	"io"
    22  	"net"
    23  	"os"
    24  	"path/filepath"
    25  	"sort"
    26  	"strconv"
    27  	"strings"
    28  	"sync"
    29  	"unicode"
    30  	"unicode/utf8"
    31  
    32  	"github.com/gohugoio/hugo/common/loggers"
    33  
    34  	"github.com/mitchellh/hashstructure"
    35  
    36  	"github.com/gohugoio/hugo/hugofs"
    37  
    38  	"github.com/gohugoio/hugo/common/hugo"
    39  
    40  	"github.com/spf13/afero"
    41  
    42  	"github.com/jdkato/prose/transform"
    43  
    44  	bp "github.com/gohugoio/hugo/bufferpool"
    45  	"github.com/spf13/pflag"
    46  )
    47  
    48  // FilePathSeparator as defined by os.Separator.
    49  const FilePathSeparator = string(filepath.Separator)
    50  
    51  // FindAvailablePort returns an available and valid TCP port.
    52  func FindAvailablePort() (*net.TCPAddr, error) {
    53  	l, err := net.Listen("tcp", ":0")
    54  	if err == nil {
    55  		defer l.Close()
    56  		addr := l.Addr()
    57  		if a, ok := addr.(*net.TCPAddr); ok {
    58  			return a, nil
    59  		}
    60  		return nil, fmt.Errorf("unable to obtain a valid tcp port: %v", addr)
    61  	}
    62  	return nil, err
    63  }
    64  
    65  // InStringArray checks if a string is an element of a slice of strings
    66  // and returns a boolean value.
    67  func InStringArray(arr []string, el string) bool {
    68  	for _, v := range arr {
    69  		if v == el {
    70  			return true
    71  		}
    72  	}
    73  	return false
    74  }
    75  
    76  // FirstUpper returns a string with the first character as upper case.
    77  func FirstUpper(s string) string {
    78  	if s == "" {
    79  		return ""
    80  	}
    81  	r, n := utf8.DecodeRuneInString(s)
    82  	return string(unicode.ToUpper(r)) + s[n:]
    83  }
    84  
    85  // UniqueStrings returns a new slice with any duplicates removed.
    86  func UniqueStrings(s []string) []string {
    87  	unique := make([]string, 0, len(s))
    88  	for i, val := range s {
    89  		var seen bool
    90  		for j := 0; j < i; j++ {
    91  			if s[j] == val {
    92  				seen = true
    93  				break
    94  			}
    95  		}
    96  		if !seen {
    97  			unique = append(unique, val)
    98  		}
    99  	}
   100  	return unique
   101  }
   102  
   103  // UniqueStringsReuse returns a slice with any duplicates removed.
   104  // It will modify the input slice.
   105  func UniqueStringsReuse(s []string) []string {
   106  	result := s[:0]
   107  	for i, val := range s {
   108  		var seen bool
   109  
   110  		for j := 0; j < i; j++ {
   111  			if s[j] == val {
   112  				seen = true
   113  				break
   114  			}
   115  		}
   116  
   117  		if !seen {
   118  			result = append(result, val)
   119  		}
   120  	}
   121  	return result
   122  }
   123  
   124  // UniqueStringsReuse returns a sorted slice with any duplicates removed.
   125  // It will modify the input slice.
   126  func UniqueStringsSorted(s []string) []string {
   127  	if len(s) == 0 {
   128  		return nil
   129  	}
   130  	ss := sort.StringSlice(s)
   131  	ss.Sort()
   132  	i := 0
   133  	for j := 1; j < len(s); j++ {
   134  		if !ss.Less(i, j) {
   135  			continue
   136  		}
   137  		i++
   138  		s[i] = s[j]
   139  	}
   140  
   141  	return s[:i+1]
   142  }
   143  
   144  // ReaderToBytes takes an io.Reader argument, reads from it
   145  // and returns bytes.
   146  func ReaderToBytes(lines io.Reader) []byte {
   147  	if lines == nil {
   148  		return []byte{}
   149  	}
   150  	b := bp.GetBuffer()
   151  	defer bp.PutBuffer(b)
   152  
   153  	b.ReadFrom(lines)
   154  
   155  	bc := make([]byte, b.Len())
   156  	copy(bc, b.Bytes())
   157  	return bc
   158  }
   159  
   160  // ReaderToString is the same as ReaderToBytes, but returns a string.
   161  func ReaderToString(lines io.Reader) string {
   162  	if lines == nil {
   163  		return ""
   164  	}
   165  	b := bp.GetBuffer()
   166  	defer bp.PutBuffer(b)
   167  	b.ReadFrom(lines)
   168  	return b.String()
   169  }
   170  
   171  // ReaderContains reports whether subslice is within r.
   172  func ReaderContains(r io.Reader, subslice []byte) bool {
   173  	if r == nil || len(subslice) == 0 {
   174  		return false
   175  	}
   176  
   177  	bufflen := len(subslice) * 4
   178  	halflen := bufflen / 2
   179  	buff := make([]byte, bufflen)
   180  	var err error
   181  	var n, i int
   182  
   183  	for {
   184  		i++
   185  		if i == 1 {
   186  			n, err = io.ReadAtLeast(r, buff[:halflen], halflen)
   187  		} else {
   188  			if i != 2 {
   189  				// shift left to catch overlapping matches
   190  				copy(buff[:], buff[halflen:])
   191  			}
   192  			n, err = io.ReadAtLeast(r, buff[halflen:], halflen)
   193  		}
   194  
   195  		if n > 0 && bytes.Contains(buff, subslice) {
   196  			return true
   197  		}
   198  
   199  		if err != nil {
   200  			break
   201  		}
   202  	}
   203  	return false
   204  }
   205  
   206  // GetTitleFunc returns a func that can be used to transform a string to
   207  // title case.
   208  //
   209  // The supported styles are
   210  //
   211  // - "Go" (strings.Title)
   212  // - "AP" (see https://www.apstylebook.com/)
   213  // - "Chicago" (see http://www.chicagomanualofstyle.org/home.html)
   214  //
   215  // If an unknown or empty style is provided, AP style is what you get.
   216  func GetTitleFunc(style string) func(s string) string {
   217  	switch strings.ToLower(style) {
   218  	case "go":
   219  		return strings.Title
   220  	case "chicago":
   221  		tc := transform.NewTitleConverter(transform.ChicagoStyle)
   222  		return tc.Title
   223  	default:
   224  		tc := transform.NewTitleConverter(transform.APStyle)
   225  		return tc.Title
   226  	}
   227  }
   228  
   229  // HasStringsPrefix tests whether the string slice s begins with prefix slice s.
   230  func HasStringsPrefix(s, prefix []string) bool {
   231  	return len(s) >= len(prefix) && compareStringSlices(s[0:len(prefix)], prefix)
   232  }
   233  
   234  // HasStringsSuffix tests whether the string slice s ends with suffix slice s.
   235  func HasStringsSuffix(s, suffix []string) bool {
   236  	return len(s) >= len(suffix) && compareStringSlices(s[len(s)-len(suffix):], suffix)
   237  }
   238  
   239  func compareStringSlices(a, b []string) bool {
   240  	if a == nil && b == nil {
   241  		return true
   242  	}
   243  
   244  	if a == nil || b == nil {
   245  		return false
   246  	}
   247  
   248  	if len(a) != len(b) {
   249  		return false
   250  	}
   251  
   252  	for i := range a {
   253  		if a[i] != b[i] {
   254  			return false
   255  		}
   256  	}
   257  
   258  	return true
   259  }
   260  
   261  // DistinctLogger ignores duplicate log statements.
   262  type DistinctLogger struct {
   263  	loggers.Logger
   264  	sync.RWMutex
   265  	m map[string]bool
   266  }
   267  
   268  func (l *DistinctLogger) Reset() {
   269  	l.Lock()
   270  	defer l.Unlock()
   271  
   272  	l.m = make(map[string]bool)
   273  }
   274  
   275  // Println will log the string returned from fmt.Sprintln given the arguments,
   276  // but not if it has been logged before.
   277  func (l *DistinctLogger) Println(v ...interface{}) {
   278  	// fmt.Sprint doesn't add space between string arguments
   279  	logStatement := strings.TrimSpace(fmt.Sprintln(v...))
   280  	l.printIfNotPrinted("println", logStatement, func() {
   281  		l.Logger.Println(logStatement)
   282  	})
   283  }
   284  
   285  // Printf will log the string returned from fmt.Sprintf given the arguments,
   286  // but not if it has been logged before.
   287  func (l *DistinctLogger) Printf(format string, v ...interface{}) {
   288  	logStatement := fmt.Sprintf(format, v...)
   289  	l.printIfNotPrinted("printf", logStatement, func() {
   290  		l.Logger.Printf(format, v...)
   291  	})
   292  }
   293  
   294  func (l *DistinctLogger) Debugf(format string, v ...interface{}) {
   295  	logStatement := fmt.Sprintf(format, v...)
   296  	l.printIfNotPrinted("debugf", logStatement, func() {
   297  		l.Logger.Debugf(format, v...)
   298  	})
   299  }
   300  
   301  func (l *DistinctLogger) Debugln(v ...interface{}) {
   302  	logStatement := fmt.Sprint(v...)
   303  	l.printIfNotPrinted("debugln", logStatement, func() {
   304  		l.Logger.Debugln(v...)
   305  	})
   306  }
   307  
   308  func (l *DistinctLogger) Infof(format string, v ...interface{}) {
   309  	logStatement := fmt.Sprintf(format, v...)
   310  	l.printIfNotPrinted("info", logStatement, func() {
   311  		l.Logger.Infof(format, v...)
   312  	})
   313  }
   314  
   315  func (l *DistinctLogger) Infoln(v ...interface{}) {
   316  	logStatement := fmt.Sprint(v...)
   317  	l.printIfNotPrinted("infoln", logStatement, func() {
   318  		l.Logger.Infoln(v...)
   319  	})
   320  }
   321  
   322  func (l *DistinctLogger) Warnf(format string, v ...interface{}) {
   323  	logStatement := fmt.Sprintf(format, v...)
   324  	l.printIfNotPrinted("warnf", logStatement, func() {
   325  		l.Logger.Warnf(format, v...)
   326  	})
   327  }
   328  
   329  func (l *DistinctLogger) Warnln(v ...interface{}) {
   330  	logStatement := fmt.Sprint(v...)
   331  	l.printIfNotPrinted("warnln", logStatement, func() {
   332  		l.Logger.Warnln(v...)
   333  	})
   334  }
   335  
   336  func (l *DistinctLogger) Errorf(format string, v ...interface{}) {
   337  	logStatement := fmt.Sprint(v...)
   338  	l.printIfNotPrinted("errorf", logStatement, func() {
   339  		l.Logger.Errorf(format, v...)
   340  	})
   341  }
   342  
   343  func (l *DistinctLogger) Errorln(v ...interface{}) {
   344  	logStatement := fmt.Sprint(v...)
   345  	l.printIfNotPrinted("errorln", logStatement, func() {
   346  		l.Logger.Errorln(v...)
   347  	})
   348  }
   349  
   350  func (l *DistinctLogger) hasPrinted(key string) bool {
   351  	l.RLock()
   352  	defer l.RUnlock()
   353  	_, found := l.m[key]
   354  	return found
   355  }
   356  
   357  func (l *DistinctLogger) printIfNotPrinted(level, logStatement string, print func()) {
   358  	key := level + logStatement
   359  	if l.hasPrinted(key) {
   360  		return
   361  	}
   362  	l.Lock()
   363  	defer l.Unlock()
   364  	l.m[key] = true // Placing this after print() can cause duplicate warning entries to be logged when --panicOnWarning is true.
   365  	print()
   366  
   367  }
   368  
   369  // NewDistinctErrorLogger creates a new DistinctLogger that logs ERRORs
   370  func NewDistinctErrorLogger() loggers.Logger {
   371  	return &DistinctLogger{m: make(map[string]bool), Logger: loggers.NewErrorLogger()}
   372  }
   373  
   374  // NewDistinctLogger creates a new DistinctLogger that logs to the provided logger.
   375  func NewDistinctLogger(logger loggers.Logger) loggers.Logger {
   376  	return &DistinctLogger{m: make(map[string]bool), Logger: logger}
   377  }
   378  
   379  // NewDistinctWarnLogger creates a new DistinctLogger that logs WARNs
   380  func NewDistinctWarnLogger() loggers.Logger {
   381  	return &DistinctLogger{m: make(map[string]bool), Logger: loggers.NewWarningLogger()}
   382  }
   383  
   384  var (
   385  	// DistinctErrorLog can be used to avoid spamming the logs with errors.
   386  	DistinctErrorLog = NewDistinctErrorLogger()
   387  
   388  	// DistinctWarnLog can be used to avoid spamming the logs with warnings.
   389  	DistinctWarnLog = NewDistinctWarnLogger()
   390  )
   391  
   392  // InitLoggers resets the global distinct loggers.
   393  func InitLoggers() {
   394  	DistinctErrorLog.Reset()
   395  	DistinctWarnLog.Reset()
   396  }
   397  
   398  // Deprecated informs about a deprecation, but only once for a given set of arguments' values.
   399  // If the err flag is enabled, it logs as an ERROR (will exit with -1) and the text will
   400  // point at the next Hugo release.
   401  // The idea is two remove an item in two Hugo releases to give users and theme authors
   402  // plenty of time to fix their templates.
   403  func Deprecated(item, alternative string, err bool) {
   404  	if err {
   405  		DistinctErrorLog.Errorf("%s is deprecated and will be removed in Hugo %s. %s", item, hugo.CurrentVersion.Next().ReleaseVersion(), alternative)
   406  	} else {
   407  		var warnPanicMessage string
   408  		if !loggers.PanicOnWarning {
   409  			warnPanicMessage = "\n\nRe-run Hugo with the flag --panicOnWarning to get a better error message."
   410  		}
   411  		DistinctWarnLog.Warnf("%s is deprecated and will be removed in a future release. %s%s", item, alternative, warnPanicMessage)
   412  	}
   413  }
   414  
   415  // SliceToLower goes through the source slice and lowers all values.
   416  func SliceToLower(s []string) []string {
   417  	if s == nil {
   418  		return nil
   419  	}
   420  
   421  	l := make([]string, len(s))
   422  	for i, v := range s {
   423  		l[i] = strings.ToLower(v)
   424  	}
   425  
   426  	return l
   427  }
   428  
   429  // MD5String takes a string and returns its MD5 hash.
   430  func MD5String(f string) string {
   431  	h := md5.New()
   432  	h.Write([]byte(f))
   433  	return hex.EncodeToString(h.Sum([]byte{}))
   434  }
   435  
   436  // MD5FromFileFast creates a MD5 hash from the given file. It only reads parts of
   437  // the file for speed, so don't use it if the files are very subtly different.
   438  // It will not close the file.
   439  func MD5FromFileFast(r io.ReadSeeker) (string, error) {
   440  	const (
   441  		// Do not change once set in stone!
   442  		maxChunks = 8
   443  		peekSize  = 64
   444  		seek      = 2048
   445  	)
   446  
   447  	h := md5.New()
   448  	buff := make([]byte, peekSize)
   449  
   450  	for i := 0; i < maxChunks; i++ {
   451  		if i > 0 {
   452  			_, err := r.Seek(seek, 0)
   453  			if err != nil {
   454  				if err == io.EOF {
   455  					break
   456  				}
   457  				return "", err
   458  			}
   459  		}
   460  
   461  		_, err := io.ReadAtLeast(r, buff, peekSize)
   462  		if err != nil {
   463  			if err == io.EOF || err == io.ErrUnexpectedEOF {
   464  				h.Write(buff)
   465  				break
   466  			}
   467  			return "", err
   468  		}
   469  		h.Write(buff)
   470  	}
   471  
   472  	return hex.EncodeToString(h.Sum(nil)), nil
   473  }
   474  
   475  // MD5FromReader creates a MD5 hash from the given reader.
   476  func MD5FromReader(r io.Reader) (string, error) {
   477  	h := md5.New()
   478  	if _, err := io.Copy(h, r); err != nil {
   479  		return "", nil
   480  	}
   481  	return hex.EncodeToString(h.Sum(nil)), nil
   482  }
   483  
   484  // IsWhitespace determines if the given rune is whitespace.
   485  func IsWhitespace(r rune) bool {
   486  	return r == ' ' || r == '\t' || r == '\n' || r == '\r'
   487  }
   488  
   489  // NormalizeHugoFlags facilitates transitions of Hugo command-line flags,
   490  // e.g. --baseUrl to --baseURL, --uglyUrls to --uglyURLs
   491  func NormalizeHugoFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
   492  	switch name {
   493  	case "baseUrl":
   494  		name = "baseURL"
   495  	case "uglyUrls":
   496  		name = "uglyURLs"
   497  	}
   498  	return pflag.NormalizedName(name)
   499  }
   500  
   501  // PrintFs prints the given filesystem to the given writer starting from the given path.
   502  // This is useful for debugging.
   503  func PrintFs(fs afero.Fs, path string, w io.Writer) {
   504  	if fs == nil {
   505  		return
   506  	}
   507  
   508  	afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error {
   509  		var filename string
   510  		var meta interface{}
   511  		if fim, ok := info.(hugofs.FileMetaInfo); ok {
   512  			filename = fim.Meta().Filename
   513  			meta = fim.Meta()
   514  		}
   515  		fmt.Fprintf(w, "    %q %q\t\t%v\n", path, filename, meta)
   516  		return nil
   517  	})
   518  }
   519  
   520  // HashString returns a hash from the given elements.
   521  // It will panic if the hash cannot be calculated.
   522  func HashString(elements ...interface{}) string {
   523  	var o interface{}
   524  	if len(elements) == 1 {
   525  		o = elements[0]
   526  	} else {
   527  		o = elements
   528  	}
   529  
   530  	hash, err := hashstructure.Hash(o, nil)
   531  	if err != nil {
   532  		panic(err)
   533  	}
   534  	return strconv.FormatUint(hash, 10)
   535  }