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 }