gitee.com/mirrors/Hugo-Go@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 }