diff options
Diffstat (limited to 'helpers/general.go')
-rw-r--r-- | helpers/general.go | 474 |
1 files changed, 474 insertions, 0 deletions
diff --git a/helpers/general.go b/helpers/general.go new file mode 100644 index 000000000..80e303087 --- /dev/null +++ b/helpers/general.go @@ -0,0 +1,474 @@ +// Copyright 2019 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "net" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "unicode" + "unicode/utf8" + + "github.com/mitchellh/hashstructure" + + "github.com/gohugoio/hugo/hugofs" + + "github.com/gohugoio/hugo/common/hugo" + + "github.com/spf13/afero" + + "github.com/jdkato/prose/transform" + + bp "github.com/gohugoio/hugo/bufferpool" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/pflag" +) + +// FilePathSeparator as defined by os.Separator. +const FilePathSeparator = string(filepath.Separator) + +// FindAvailablePort returns an available and valid TCP port. +func FindAvailablePort() (*net.TCPAddr, error) { + l, err := net.Listen("tcp", ":0") + if err == nil { + defer l.Close() + addr := l.Addr() + if a, ok := addr.(*net.TCPAddr); ok { + return a, nil + } + return nil, fmt.Errorf("unable to obtain a valid tcp port: %v", addr) + } + return nil, err +} + +// InStringArray checks if a string is an element of a slice of strings +// and returns a boolean value. +func InStringArray(arr []string, el string) bool { + for _, v := range arr { + if v == el { + return true + } + } + return false +} + +// FirstUpper returns a string with the first character as upper case. +func FirstUpper(s string) string { + if s == "" { + return "" + } + r, n := utf8.DecodeRuneInString(s) + return string(unicode.ToUpper(r)) + s[n:] +} + +// UniqueStrings returns a new slice with any duplicates removed. +func UniqueStrings(s []string) []string { + unique := make([]string, 0, len(s)) + set := map[string]interface{}{} + for _, val := range s { + if _, ok := set[val]; !ok { + unique = append(unique, val) + set[val] = val + } + } + return unique +} + +// UniqueStringsReuse returns a slice with any duplicates removed. +// It will modify the input slice. +func UniqueStringsReuse(s []string) []string { + set := map[string]interface{}{} + result := s[:0] + for _, val := range s { + if _, ok := set[val]; !ok { + result = append(result, val) + set[val] = val + } + } + return result +} + +// UniqueStringsReuse returns a sorted slice with any duplicates removed. +// It will modify the input slice. +func UniqueStringsSorted(s []string) []string { + if len(s) == 0 { + return nil + } + ss := sort.StringSlice(s) + ss.Sort() + i := 0 + for j := 1; j < len(s); j++ { + if !ss.Less(i, j) { + continue + } + i++ + s[i] = s[j] + } + + return s[:i+1] +} + +// ReaderToBytes takes an io.Reader argument, reads from it +// and returns bytes. +func ReaderToBytes(lines io.Reader) []byte { + if lines == nil { + return []byte{} + } + b := bp.GetBuffer() + defer bp.PutBuffer(b) + + b.ReadFrom(lines) + + bc := make([]byte, b.Len()) + copy(bc, b.Bytes()) + return bc +} + +// ReaderToString is the same as ReaderToBytes, but returns a string. +func ReaderToString(lines io.Reader) string { + if lines == nil { + return "" + } + b := bp.GetBuffer() + defer bp.PutBuffer(b) + b.ReadFrom(lines) + return b.String() +} + +// ReaderContains reports whether subslice is within r. +func ReaderContains(r io.Reader, subslice []byte) bool { + + if r == nil || len(subslice) == 0 { + return false + } + + bufflen := len(subslice) * 4 + halflen := bufflen / 2 + buff := make([]byte, bufflen) + var err error + var n, i int + + for { + i++ + if i == 1 { + n, err = io.ReadAtLeast(r, buff[:halflen], halflen) + } else { + if i != 2 { + // shift left to catch overlapping matches + copy(buff[:], buff[halflen:]) + } + n, err = io.ReadAtLeast(r, buff[halflen:], halflen) + } + + if n > 0 && bytes.Contains(buff, subslice) { + return true + } + + if err != nil { + break + } + } + return false +} + +// GetTitleFunc returns a func that can be used to transform a string to +// title case. +// +// The supported styles are +// +// - "Go" (strings.Title) +// - "AP" (see https://www.apstylebook.com/) +// - "Chicago" (see http://www.chicagomanualofstyle.org/home.html) +// +// If an unknown or empty style is provided, AP style is what you get. +func GetTitleFunc(style string) func(s string) string { + switch strings.ToLower(style) { + case "go": + return strings.Title + case "chicago": + tc := transform.NewTitleConverter(transform.ChicagoStyle) + return tc.Title + default: + tc := transform.NewTitleConverter(transform.APStyle) + return tc.Title + } +} + +// HasStringsPrefix tests whether the string slice s begins with prefix slice s. +func HasStringsPrefix(s, prefix []string) bool { + return len(s) >= len(prefix) && compareStringSlices(s[0:len(prefix)], prefix) +} + +// HasStringsSuffix tests whether the string slice s ends with suffix slice s. +func HasStringsSuffix(s, suffix []string) bool { + return len(s) >= len(suffix) && compareStringSlices(s[len(s)-len(suffix):], suffix) +} + +func compareStringSlices(a, b []string) bool { + if a == nil && b == nil { + return true + } + + if a == nil || b == nil { + return false + } + + if len(a) != len(b) { + return false + } + + for i := range a { + if a[i] != b[i] { + return false + } + } + + return true +} + +// LogPrinter is the common interface of the JWWs loggers. +type LogPrinter interface { + // Println is the only common method that works in all of JWWs loggers. + Println(a ...interface{}) +} + +// DistinctLogger ignores duplicate log statements. +type DistinctLogger struct { + sync.RWMutex + getLogger func() LogPrinter + m map[string]bool +} + +func (l *DistinctLogger) Reset() { + l.Lock() + defer l.Unlock() + + l.m = make(map[string]bool) +} + +// Println will log the string returned from fmt.Sprintln given the arguments, +// but not if it has been logged before. +func (l *DistinctLogger) Println(v ...interface{}) { + // fmt.Sprint doesn't add space between string arguments + logStatement := strings.TrimSpace(fmt.Sprintln(v...)) + l.print(logStatement) +} + +// Printf will log the string returned from fmt.Sprintf given the arguments, +// but not if it has been logged before. +// Note: A newline is appended. +func (l *DistinctLogger) Printf(format string, v ...interface{}) { + logStatement := fmt.Sprintf(format, v...) + l.print(logStatement) +} + +func (l *DistinctLogger) print(logStatement string) { + l.RLock() + if l.m[logStatement] { + l.RUnlock() + return + } + l.RUnlock() + + l.Lock() + if !l.m[logStatement] { + l.getLogger().Println(logStatement) + l.m[logStatement] = true + } + l.Unlock() +} + +// NewDistinctErrorLogger creates a new DistinctLogger that logs ERRORs +func NewDistinctErrorLogger() *DistinctLogger { + return &DistinctLogger{m: make(map[string]bool), getLogger: func() LogPrinter { return jww.ERROR }} +} + +// NewDistinctLogger creates a new DistinctLogger that logs to the provided logger. +func NewDistinctLogger(logger LogPrinter) *DistinctLogger { + return &DistinctLogger{m: make(map[string]bool), getLogger: func() LogPrinter { return logger }} +} + +// NewDistinctWarnLogger creates a new DistinctLogger that logs WARNs +func NewDistinctWarnLogger() *DistinctLogger { + return &DistinctLogger{m: make(map[string]bool), getLogger: func() LogPrinter { return jww.WARN }} +} + +// NewDistinctFeedbackLogger creates a new DistinctLogger that can be used +// to give feedback to the user while not spamming with duplicates. +func NewDistinctFeedbackLogger() *DistinctLogger { + return &DistinctLogger{m: make(map[string]bool), getLogger: func() LogPrinter { return jww.FEEDBACK }} +} + +var ( + // DistinctErrorLog can be used to avoid spamming the logs with errors. + DistinctErrorLog = NewDistinctErrorLogger() + + // DistinctWarnLog can be used to avoid spamming the logs with warnings. + DistinctWarnLog = NewDistinctWarnLogger() + + // DistinctFeedbackLog can be used to avoid spamming the logs with info messages. + DistinctFeedbackLog = NewDistinctFeedbackLogger() +) + +// InitLoggers resets the global distinct loggers. +func InitLoggers() { + DistinctErrorLog.Reset() + DistinctWarnLog.Reset() + DistinctFeedbackLog.Reset() +} + +// Deprecated informs about a deprecation, but only once for a given set of arguments' values. +// If the err flag is enabled, it logs as an ERROR (will exit with -1) and the text will +// point at the next Hugo release. +// The idea is two remove an item in two Hugo releases to give users and theme authors +// plenty of time to fix their templates. +func Deprecated(item, alternative string, err bool) { + if err { + DistinctErrorLog.Printf("%s is deprecated and will be removed in Hugo %s. %s", item, hugo.CurrentVersion.Next().ReleaseVersion(), alternative) + + } else { + DistinctWarnLog.Printf("%s is deprecated and will be removed in a future release. %s", item, alternative) + } +} + +// SliceToLower goes through the source slice and lowers all values. +func SliceToLower(s []string) []string { + if s == nil { + return nil + } + + l := make([]string, len(s)) + for i, v := range s { + l[i] = strings.ToLower(v) + } + + return l +} + +// MD5String takes a string and returns its MD5 hash. +func MD5String(f string) string { + h := md5.New() + h.Write([]byte(f)) + return hex.EncodeToString(h.Sum([]byte{})) +} + +// MD5FromFileFast creates a MD5 hash from the given file. It only reads parts of +// the file for speed, so don't use it if the files are very subtly different. +// It will not close the file. +func MD5FromFileFast(r io.ReadSeeker) (string, error) { + const ( + // Do not change once set in stone! + maxChunks = 8 + peekSize = 64 + seek = 2048 + ) + + h := md5.New() + buff := make([]byte, peekSize) + + for i := 0; i < maxChunks; i++ { + if i > 0 { + _, err := r.Seek(seek, 0) + if err != nil { + if err == io.EOF { + break + } + return "", err + } + } + + _, err := io.ReadAtLeast(r, buff, peekSize) + if err != nil { + if err == io.EOF || err == io.ErrUnexpectedEOF { + h.Write(buff) + break + } + return "", err + } + h.Write(buff) + } + + return hex.EncodeToString(h.Sum(nil)), nil +} + +// MD5FromReader creates a MD5 hash from the given reader. +func MD5FromReader(r io.Reader) (string, error) { + h := md5.New() + if _, err := io.Copy(h, r); err != nil { + return "", nil + } + return hex.EncodeToString(h.Sum(nil)), nil +} + +// IsWhitespace determines if the given rune is whitespace. +func IsWhitespace(r rune) bool { + return r == ' ' || r == '\t' || r == '\n' || r == '\r' +} + +// NormalizeHugoFlags facilitates transitions of Hugo command-line flags, +// e.g. --baseUrl to --baseURL, --uglyUrls to --uglyURLs +func NormalizeHugoFlags(f *pflag.FlagSet, name string) pflag.NormalizedName { + switch name { + case "baseUrl": + name = "baseURL" + case "uglyUrls": + name = "uglyURLs" + } + return pflag.NormalizedName(name) +} + +// PrintFs prints the given filesystem to the given writer starting from the given path. +// This is useful for debugging. +func PrintFs(fs afero.Fs, path string, w io.Writer) { + if fs == nil { + return + } + + afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error { + var filename string + var meta interface{} + if fim, ok := info.(hugofs.FileMetaInfo); ok { + filename = fim.Meta().Filename() + meta = fim.Meta() + } + fmt.Fprintf(w, " %q %q\t\t%v\n", path, filename, meta) + return nil + }) +} + +// HashString returns a hash from the given elements. +// It will panic if the hash cannot be calculated. +func HashString(elements ...interface{}) string { + var o interface{} + if len(elements) == 1 { + o = elements[0] + } else { + o = elements + } + + hash, err := hashstructure.Hash(o, nil) + if err != nil { + panic(err) + } + return strconv.FormatUint(hash, 10) +} |