mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-29 13:32:08 -05:00
de7c32a1a8
This commit moves almost all of the template functions into separate packages under tpl/ and adds a namespace framework. All changes should be backward compatible for end users, as all existing function names in the template funcMap are left intact. Seq and DoArithmatic have been moved out of the helpers package and into template namespaces. Most of the tests involved have been refactored, and many new tests have been written. There's still work to do, but this is a big improvement. I got a little overzealous and added some new functions along the way: - strings.Contains - strings.ContainsAny - strings.HasSuffix - strings.TrimPrefix - strings.TrimSuffix Documentation is forthcoming. Fixes #3042
357 lines
9 KiB
Go
357 lines
9 KiB
Go
// 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"
|
|
|
|
"github.com/spf13/cast"
|
|
bp "github.com/spf13/hugo/bufferpool"
|
|
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
|
|
}
|