github.com/dominikszabo/hugo-ds-clean@v0.47.1/helpers/general.go (about)

     1  // Copyright 2015 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  	"strings"
    26  	"sync"
    27  	"unicode"
    28  	"unicode/utf8"
    29  
    30  	"github.com/gohugoio/hugo/hugofs"
    31  
    32  	"github.com/spf13/afero"
    33  
    34  	"github.com/jdkato/prose/transform"
    35  
    36  	bp "github.com/gohugoio/hugo/bufferpool"
    37  	jww "github.com/spf13/jwalterweatherman"
    38  	"github.com/spf13/pflag"
    39  )
    40  
    41  // FilePathSeparator as defined by os.Separator.
    42  const FilePathSeparator = string(filepath.Separator)
    43  
    44  // Strips carriage returns from third-party / external processes (useful for Windows)
    45  func normalizeExternalHelperLineFeeds(content []byte) []byte {
    46  	return bytes.Replace(content, []byte("\r"), []byte(""), -1)
    47  }
    48  
    49  // FindAvailablePort returns an available and valid TCP port.
    50  func FindAvailablePort() (*net.TCPAddr, error) {
    51  	l, err := net.Listen("tcp", ":0")
    52  	if err == nil {
    53  		defer l.Close()
    54  		addr := l.Addr()
    55  		if a, ok := addr.(*net.TCPAddr); ok {
    56  			return a, nil
    57  		}
    58  		return nil, fmt.Errorf("Unable to obtain a valid tcp port. %v", addr)
    59  	}
    60  	return nil, err
    61  }
    62  
    63  // InStringArray checks if a string is an element of a slice of strings
    64  // and returns a boolean value.
    65  func InStringArray(arr []string, el string) bool {
    66  	for _, v := range arr {
    67  		if v == el {
    68  			return true
    69  		}
    70  	}
    71  	return false
    72  }
    73  
    74  // GuessType attempts to guess the type of file from a given string.
    75  func GuessType(in string) string {
    76  	switch strings.ToLower(in) {
    77  	case "md", "markdown", "mdown":
    78  		return "markdown"
    79  	case "asciidoc", "adoc", "ad":
    80  		return "asciidoc"
    81  	case "mmark":
    82  		return "mmark"
    83  	case "rst":
    84  		return "rst"
    85  	case "pandoc", "pdc":
    86  		return "pandoc"
    87  	case "html", "htm":
    88  		return "html"
    89  	case "org":
    90  		return "org"
    91  	}
    92  
    93  	return "unknown"
    94  }
    95  
    96  // FirstUpper returns a string with the first character as upper case.
    97  func FirstUpper(s string) string {
    98  	if s == "" {
    99  		return ""
   100  	}
   101  	r, n := utf8.DecodeRuneInString(s)
   102  	return string(unicode.ToUpper(r)) + s[n:]
   103  }
   104  
   105  // UniqueStrings returns a new slice with any duplicates removed.
   106  func UniqueStrings(s []string) []string {
   107  	var unique []string
   108  	set := map[string]interface{}{}
   109  	for _, val := range s {
   110  		if _, ok := set[val]; !ok {
   111  			unique = append(unique, val)
   112  			set[val] = val
   113  		}
   114  	}
   115  	return unique
   116  }
   117  
   118  // ReaderToBytes takes an io.Reader argument, reads from it
   119  // and returns bytes.
   120  func ReaderToBytes(lines io.Reader) []byte {
   121  	if lines == nil {
   122  		return []byte{}
   123  	}
   124  	b := bp.GetBuffer()
   125  	defer bp.PutBuffer(b)
   126  
   127  	b.ReadFrom(lines)
   128  
   129  	bc := make([]byte, b.Len(), b.Len())
   130  	copy(bc, b.Bytes())
   131  	return bc
   132  }
   133  
   134  // ReaderToString is the same as ReaderToBytes, but returns a string.
   135  func ReaderToString(lines io.Reader) string {
   136  	if lines == nil {
   137  		return ""
   138  	}
   139  	b := bp.GetBuffer()
   140  	defer bp.PutBuffer(b)
   141  	b.ReadFrom(lines)
   142  	return b.String()
   143  }
   144  
   145  // ReaderContains reports whether subslice is within r.
   146  func ReaderContains(r io.Reader, subslice []byte) bool {
   147  
   148  	if r == nil || len(subslice) == 0 {
   149  		return false
   150  	}
   151  
   152  	bufflen := len(subslice) * 4
   153  	halflen := bufflen / 2
   154  	buff := make([]byte, bufflen)
   155  	var err error
   156  	var n, i int
   157  
   158  	for {
   159  		i++
   160  		if i == 1 {
   161  			n, err = io.ReadAtLeast(r, buff[:halflen], halflen)
   162  		} else {
   163  			if i != 2 {
   164  				// shift left to catch overlapping matches
   165  				copy(buff[:], buff[halflen:])
   166  			}
   167  			n, err = io.ReadAtLeast(r, buff[halflen:], halflen)
   168  		}
   169  
   170  		if n > 0 && bytes.Contains(buff, subslice) {
   171  			return true
   172  		}
   173  
   174  		if err != nil {
   175  			break
   176  		}
   177  	}
   178  	return false
   179  }
   180  
   181  // GetTitleFunc returns a func that can be used to transform a string to
   182  // title case.
   183  //
   184  // The supported styles are
   185  //
   186  // - "Go" (strings.Title)
   187  // - "AP" (see https://www.apstylebook.com/)
   188  // - "Chicago" (see http://www.chicagomanualofstyle.org/home.html)
   189  //
   190  // If an unknown or empty style is provided, AP style is what you get.
   191  func GetTitleFunc(style string) func(s string) string {
   192  	switch strings.ToLower(style) {
   193  	case "go":
   194  		return strings.Title
   195  	case "chicago":
   196  		tc := transform.NewTitleConverter(transform.ChicagoStyle)
   197  		return tc.Title
   198  	default:
   199  		tc := transform.NewTitleConverter(transform.APStyle)
   200  		return tc.Title
   201  	}
   202  }
   203  
   204  // HasStringsPrefix tests whether the string slice s begins with prefix slice s.
   205  func HasStringsPrefix(s, prefix []string) bool {
   206  	return len(s) >= len(prefix) && compareStringSlices(s[0:len(prefix)], prefix)
   207  }
   208  
   209  // HasStringsSuffix tests whether the string slice s ends with suffix slice s.
   210  func HasStringsSuffix(s, suffix []string) bool {
   211  	return len(s) >= len(suffix) && compareStringSlices(s[len(s)-len(suffix):], suffix)
   212  }
   213  
   214  func compareStringSlices(a, b []string) bool {
   215  	if a == nil && b == nil {
   216  		return true
   217  	}
   218  
   219  	if a == nil || b == nil {
   220  		return false
   221  	}
   222  
   223  	if len(a) != len(b) {
   224  		return false
   225  	}
   226  
   227  	for i := range a {
   228  		if a[i] != b[i] {
   229  			return false
   230  		}
   231  	}
   232  
   233  	return true
   234  }
   235  
   236  // LogPrinter is the common interface of the JWWs loggers.
   237  type LogPrinter interface {
   238  	// Println is the only common method that works in all of JWWs loggers.
   239  	Println(a ...interface{})
   240  }
   241  
   242  // DistinctLogger ignores duplicate log statements.
   243  type DistinctLogger struct {
   244  	sync.RWMutex
   245  	logger LogPrinter
   246  	m      map[string]bool
   247  }
   248  
   249  // Println will log the string returned from fmt.Sprintln given the arguments,
   250  // but not if it has been logged before.
   251  func (l *DistinctLogger) Println(v ...interface{}) {
   252  	// fmt.Sprint doesn't add space between string arguments
   253  	logStatement := strings.TrimSpace(fmt.Sprintln(v...))
   254  	l.print(logStatement)
   255  }
   256  
   257  // Printf will log the string returned from fmt.Sprintf given the arguments,
   258  // but not if it has been logged before.
   259  // Note: A newline is appended.
   260  func (l *DistinctLogger) Printf(format string, v ...interface{}) {
   261  	logStatement := fmt.Sprintf(format, v...)
   262  	l.print(logStatement)
   263  }
   264  
   265  func (l *DistinctLogger) print(logStatement string) {
   266  	l.RLock()
   267  	if l.m[logStatement] {
   268  		l.RUnlock()
   269  		return
   270  	}
   271  	l.RUnlock()
   272  
   273  	l.Lock()
   274  	if !l.m[logStatement] {
   275  		l.logger.Println(logStatement)
   276  		l.m[logStatement] = true
   277  	}
   278  	l.Unlock()
   279  }
   280  
   281  // NewDistinctErrorLogger creates a new DistinctLogger that logs ERRORs
   282  func NewDistinctErrorLogger() *DistinctLogger {
   283  	return &DistinctLogger{m: make(map[string]bool), logger: jww.ERROR}
   284  }
   285  
   286  // NewDistinctLogger creates a new DistinctLogger that logs to the provided logger.
   287  func NewDistinctLogger(logger LogPrinter) *DistinctLogger {
   288  	return &DistinctLogger{m: make(map[string]bool), logger: logger}
   289  }
   290  
   291  // NewDistinctWarnLogger creates a new DistinctLogger that logs WARNs
   292  func NewDistinctWarnLogger() *DistinctLogger {
   293  	return &DistinctLogger{m: make(map[string]bool), logger: jww.WARN}
   294  }
   295  
   296  // NewDistinctFeedbackLogger creates a new DistinctLogger that can be used
   297  // to give feedback to the user while not spamming with duplicates.
   298  func NewDistinctFeedbackLogger() *DistinctLogger {
   299  	return &DistinctLogger{m: make(map[string]bool), logger: jww.FEEDBACK}
   300  }
   301  
   302  var (
   303  	// DistinctErrorLog can be used to avoid spamming the logs with errors.
   304  	DistinctErrorLog = NewDistinctErrorLogger()
   305  
   306  	// DistinctWarnLog can be used to avoid spamming the logs with warnings.
   307  	DistinctWarnLog = NewDistinctWarnLogger()
   308  
   309  	// DistinctFeedbackLog can be used to avoid spamming the logs with info messages.
   310  	DistinctFeedbackLog = NewDistinctFeedbackLogger()
   311  )
   312  
   313  // InitLoggers sets up the global distinct loggers.
   314  func InitLoggers() {
   315  	DistinctErrorLog = NewDistinctErrorLogger()
   316  	DistinctWarnLog = NewDistinctWarnLogger()
   317  	DistinctFeedbackLog = NewDistinctFeedbackLogger()
   318  }
   319  
   320  // Deprecated informs about a deprecation, but only once for a given set of arguments' values.
   321  // If the err flag is enabled, it logs as an ERROR (will exit with -1) and the text will
   322  // point at the next Hugo release.
   323  // The idea is two remove an item in two Hugo releases to give users and theme authors
   324  // plenty of time to fix their templates.
   325  func Deprecated(object, item, alternative string, err bool) {
   326  	if err {
   327  		DistinctErrorLog.Printf("%s's %s is deprecated and will be removed in Hugo %s. %s", object, item, CurrentHugoVersion.Next().ReleaseVersion(), alternative)
   328  
   329  	} else {
   330  		// Make sure the users see this while avoiding build breakage. This will not lead to an os.Exit(-1)
   331  		DistinctFeedbackLog.Printf("WARNING: %s's %s is deprecated and will be removed in a future release. %s", object, item, alternative)
   332  	}
   333  }
   334  
   335  // SliceToLower goes through the source slice and lowers all values.
   336  func SliceToLower(s []string) []string {
   337  	if s == nil {
   338  		return nil
   339  	}
   340  
   341  	l := make([]string, len(s))
   342  	for i, v := range s {
   343  		l[i] = strings.ToLower(v)
   344  	}
   345  
   346  	return l
   347  }
   348  
   349  // MD5String takes a string and returns its MD5 hash.
   350  func MD5String(f string) string {
   351  	h := md5.New()
   352  	h.Write([]byte(f))
   353  	return hex.EncodeToString(h.Sum([]byte{}))
   354  }
   355  
   356  // MD5FromFileFast creates a MD5 hash from the given file. It only reads parts of
   357  // the file for speed, so don't use it if the files are very subtly different.
   358  // It will not close the file.
   359  func MD5FromFileFast(r io.ReadSeeker) (string, error) {
   360  	const (
   361  		// Do not change once set in stone!
   362  		maxChunks = 8
   363  		peekSize  = 64
   364  		seek      = 2048
   365  	)
   366  
   367  	h := md5.New()
   368  	buff := make([]byte, peekSize)
   369  
   370  	for i := 0; i < maxChunks; i++ {
   371  		if i > 0 {
   372  			_, err := r.Seek(seek, 0)
   373  			if err != nil {
   374  				if err == io.EOF {
   375  					break
   376  				}
   377  				return "", err
   378  			}
   379  		}
   380  
   381  		_, err := io.ReadAtLeast(r, buff, peekSize)
   382  		if err != nil {
   383  			if err == io.EOF || err == io.ErrUnexpectedEOF {
   384  				h.Write(buff)
   385  				break
   386  			}
   387  			return "", err
   388  		}
   389  		h.Write(buff)
   390  	}
   391  
   392  	return hex.EncodeToString(h.Sum(nil)), nil
   393  }
   394  
   395  // MD5FromFile creates a MD5 hash from the given file.
   396  // It will not close the file.
   397  func MD5FromFile(f afero.File) (string, error) {
   398  	h := md5.New()
   399  	if _, err := io.Copy(h, f); err != nil {
   400  		return "", nil
   401  	}
   402  	return hex.EncodeToString(h.Sum(nil)), nil
   403  }
   404  
   405  // IsWhitespace determines if the given rune is whitespace.
   406  func IsWhitespace(r rune) bool {
   407  	return r == ' ' || r == '\t' || r == '\n' || r == '\r'
   408  }
   409  
   410  // NormalizeHugoFlags facilitates transitions of Hugo command-line flags,
   411  // e.g. --baseUrl to --baseURL, --uglyUrls to --uglyURLs
   412  func NormalizeHugoFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
   413  	switch name {
   414  	case "baseUrl":
   415  		name = "baseURL"
   416  		break
   417  	case "uglyUrls":
   418  		name = "uglyURLs"
   419  		break
   420  	}
   421  	return pflag.NormalizedName(name)
   422  }
   423  
   424  // DiffStringSlices returns the difference between two string slices.
   425  // Useful in tests.
   426  // See:
   427  // http://stackoverflow.com/questions/19374219/how-to-find-the-difference-between-two-slices-of-strings-in-golang
   428  func DiffStringSlices(slice1 []string, slice2 []string) []string {
   429  	diffStr := []string{}
   430  	m := map[string]int{}
   431  
   432  	for _, s1Val := range slice1 {
   433  		m[s1Val] = 1
   434  	}
   435  	for _, s2Val := range slice2 {
   436  		m[s2Val] = m[s2Val] + 1
   437  	}
   438  
   439  	for mKey, mVal := range m {
   440  		if mVal == 1 {
   441  			diffStr = append(diffStr, mKey)
   442  		}
   443  	}
   444  
   445  	return diffStr
   446  }
   447  
   448  // DiffString splits the strings into fields and runs it into DiffStringSlices.
   449  // Useful for tests.
   450  func DiffStrings(s1, s2 string) []string {
   451  	return DiffStringSlices(strings.Fields(s1), strings.Fields(s2))
   452  }
   453  
   454  // PrintFs prints the given filesystem to the given writer starting from the given path.
   455  // This is useful for debugging.
   456  func PrintFs(fs afero.Fs, path string, w io.Writer) {
   457  	if fs == nil {
   458  		return
   459  	}
   460  	afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error {
   461  		if info != nil && !info.IsDir() {
   462  			s := path
   463  			if lang, ok := info.(hugofs.LanguageAnnouncer); ok {
   464  				s = s + "\tLANG: " + lang.Lang()
   465  			}
   466  			if fp, ok := info.(hugofs.FilePather); ok {
   467  				s = s + "\tRF: " + fp.Filename() + "\tBP: " + fp.BaseDir()
   468  			}
   469  			fmt.Fprintln(w, "    ", s)
   470  		}
   471  		return nil
   472  	})
   473  }