diff options
Diffstat (limited to 'helpers/general.go')
-rw-r--r-- | helpers/general.go | 357 |
1 files changed, 357 insertions, 0 deletions
diff --git a/helpers/general.go b/helpers/general.go new file mode 100644 index 000000000..7901be654 --- /dev/null +++ b/helpers/general.go @@ -0,0 +1,357 @@ +// Copyright 2015 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" + "path/filepath" + "strings" + "sync" + "unicode" + "unicode/utf8" + + bp "github.com/gohugoio/hugo/bufferpool" + "github.com/spf13/cast" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/pflag" +) + +// FilePathSeparator as defined by os.Separator. +const FilePathSeparator = string(filepath.Separator) + +// Strips carriage returns from third-party / external processes (useful for Windows) +func normalizeExternalHelperLineFeeds(content []byte) []byte { + return bytes.Replace(content, []byte("\r"), []byte(""), -1) +} + +// 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 +} + +// GuessType attempts to guess the type of file from a given string. +func GuessType(in string) string { + switch strings.ToLower(in) { + case "md", "markdown", "mdown": + return "markdown" + case "asciidoc", "adoc", "ad": + return "asciidoc" + case "mmark": + return "mmark" + case "rst": + return "rst" + case "html", "htm": + return "html" + case "org": + return "org" + } + + return "unknown" +} + +// 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 { + var unique []string + set := map[string]interface{}{} + for _, val := range s { + if _, ok := set[val]; !ok { + unique = append(unique, val) + set[val] = val + } + } + return unique +} + +// 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(), b.Len()) + copy(bc, b.Bytes()) + return bc +} + +// ToLowerMap makes all the keys in the given map lower cased and will do so +// recursively. +// Notes: +// * This will modify the map given. +// * Any nested map[interface{}]interface{} will be converted to map[string]interface{}. +func ToLowerMap(m map[string]interface{}) { + for k, v := range m { + switch v.(type) { + case map[interface{}]interface{}: + v = cast.ToStringMap(v) + ToLowerMap(v.(map[string]interface{})) + case map[string]interface{}: + ToLowerMap(v.(map[string]interface{})) + } + + lKey := strings.ToLower(k) + if k != lKey { + delete(m, k) + m[lKey] = v + } + + } +} + +// 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 +} + +// ThemeSet checks whether a theme is in use or not. +func (p *PathSpec) ThemeSet() bool { + return p.theme != "" +} + +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 + logger logPrinter + m 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.logger.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), logger: jww.ERROR} +} + +// NewDistinctWarnLogger creates a new DistinctLogger that logs WARNs +func NewDistinctWarnLogger() *DistinctLogger { + return &DistinctLogger{m: make(map[string]bool), logger: 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), logger: 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 sets up the global distinct loggers. +func InitLoggers() { + DistinctErrorLog = NewDistinctErrorLogger() + DistinctWarnLog = NewDistinctWarnLogger() + DistinctFeedbackLog = NewDistinctFeedbackLogger() +} + +// 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(object, item, alternative string, err bool) { + if err { + DistinctErrorLog.Printf("%s's %s is deprecated and will be removed in Hugo %s. %s.", object, item, CurrentHugoVersion.Next().ReleaseVersion(), alternative) + + } else { + // Make sure the users see this while avoiding build breakage. This will not lead to an os.Exit(-1) + DistinctFeedbackLog.Printf("WARNING: %s's %s is deprecated and will be removed in a future release. %s.", object, 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{})) +} + +// 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" + break + case "uglyUrls": + name = "uglyURLs" + break + } + return pflag.NormalizedName(name) +} + +// DiffStringSlices returns the difference between two string slices. +// Useful in tests. +// See: +// http://stackoverflow.com/questions/19374219/how-to-find-the-difference-between-two-slices-of-strings-in-golang +func DiffStringSlices(slice1 []string, slice2 []string) []string { + diffStr := []string{} + m := map[string]int{} + + for _, s1Val := range slice1 { + m[s1Val] = 1 + } + for _, s2Val := range slice2 { + m[s2Val] = m[s2Val] + 1 + } + + for mKey, mVal := range m { + if mVal == 1 { + diffStr = append(diffStr, mKey) + } + } + + return diffStr +} |