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