// Copyright 2019 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 collections
import (
"fmt"
"reflect"
)
// Append appends from to a slice to and returns the resulting slice.
// If length of from is one and the only element is a slice of same type as to,
// it will be appended.
func Append(to any, from ...any) (any, error) {
if len(from) == 0 {
return to, nil
}
tov, toIsNil := indirect(reflect.ValueOf(to))
toIsNil = toIsNil || to == nil
var tot reflect.Type
if !toIsNil {
if tov.Kind() == reflect.Slice {
// Create a copy of tov, so we don't modify the original.
c := reflect.MakeSlice(tov.Type(), tov.Len(), tov.Len()+len(from))
reflect.Copy(c, tov)
tov = c
}
if tov.Kind() != reflect.Slice {
return nil, fmt.Errorf("expected a slice, got %T", to)
}
tot = tov.Type().Elem()
if tot.Kind() == reflect.Slice {
totvt := tot.Elem()
fromvs := make([]reflect.Value, len(from))
for i, f := range from {
fromv := reflect.ValueOf(f)
fromt := fromv.Type()
if fromt.Kind() == reflect.Slice {
fromt = fromt.Elem()
}
if totvt != fromt {
return nil, fmt.Errorf("cannot append slice of %s to slice of %s", fromt, totvt)
} else {
fromvs[i] = fromv
}
}
return reflect.Append(tov, fromvs...).Interface(), nil
}
toIsNil = tov.Len() == 0
if len(from) == 1 {
fromv := reflect.ValueOf(from[0])
if !fromv.IsValid() {
// from[0] is nil
return appendToInterfaceSliceFromValues(tov, fromv)
}
fromt := fromv.Type()
if fromt.Kind() == reflect.Slice {
fromt = fromt.Elem()
}
if fromv.Kind() == reflect.Slice {
if toIsNil {
// If we get nil []string, we just return the []string
return from[0], nil
}
// If we get []string []string, we append the from slice to to
if tot == fromt {
return reflect.AppendSlice(tov, fromv).Interface(), nil
} else if !fromt.AssignableTo(tot) {
// Fall back to a []interface{} slice.
return appendToInterfaceSliceFromValues(tov, fromv)
}
}
}
}
if toIsNil {
return Slice(from...), nil
}
for _, f := range from {
fv := reflect.ValueOf(f)
if !fv.IsValid() || !fv.Type().AssignableTo(tot) {
// Fall back to a []interface{} slice.
tov, _ := indirect(reflect.ValueOf(to))
return appendToInterfaceSlice(tov, from...)
}
tov = reflect.Append(tov, fv)
}
return tov.Interface(), nil
}
func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, error) {
var tos []any
for _, slice := range []reflect.Value{slice1, slice2} {
if !slice.IsValid() {
tos = append(tos, nil)
continue
}
for i := range slice.Len() {
tos = append(tos, slice.Index(i).Interface())
}
}
return tos, nil
}
func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
var tos []any
for i := range tov.Len() {
tos = append(tos, tov.Index(i).Interface())
}
tos = append(tos, from...)
return tos, nil
}
// indirect is borrowed from the Go stdlib: 'text/template/exec.go'
// TODO(bep) consolidate
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
if v.IsNil() {
return v, true
}
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
break
}
}
return v, false
}
// Copyright 2018 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 collections
import (
"reflect"
"sort"
)
// Slicer defines a very generic way to create a typed slice. This is used
// in collections.Slice template func to get types such as Pages, PageGroups etc.
// instead of the less useful []interface{}.
type Slicer interface {
Slice(items any) (any, error)
}
// Slice returns a slice of all passed arguments.
func Slice(args ...any) any {
if len(args) == 0 {
return args
}
first := args[0]
firstType := reflect.TypeOf(first)
if firstType == nil {
return args
}
if g, ok := first.(Slicer); ok {
v, err := g.Slice(args)
if err == nil {
return v
}
// If Slice fails, the items are not of the same type and
// []interface{} is the best we can do.
return args
}
if len(args) > 1 {
// This can be a mix of types.
for i := 1; i < len(args); i++ {
if firstType != reflect.TypeOf(args[i]) {
// []interface{} is the best we can do
return args
}
}
}
slice := reflect.MakeSlice(reflect.SliceOf(firstType), len(args), len(args))
for i, arg := range args {
slice.Index(i).Set(reflect.ValueOf(arg))
}
return slice.Interface()
}
// StringSliceToInterfaceSlice converts ss to []interface{}.
func StringSliceToInterfaceSlice(ss []string) []any {
result := make([]any, len(ss))
for i, s := range ss {
result[i] = s
}
return result
}
type SortedStringSlice []string
// Contains returns true if s is in ss.
func (ss SortedStringSlice) Contains(s string) bool {
i := sort.SearchStrings(ss, s)
return i < len(ss) && ss[i] == s
}
// Count returns the number of times s is in ss.
func (ss SortedStringSlice) Count(s string) int {
var count int
i := sort.SearchStrings(ss, s)
for i < len(ss) && ss[i] == s {
count++
i++
}
return count
}
// Copyright 2024 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 collections
import "slices"
import "sync"
// Stack is a simple LIFO stack that is safe for concurrent use.
type Stack[T any] struct {
items []T
zero T
mu sync.RWMutex
}
func NewStack[T any]() *Stack[T] {
return &Stack[T]{}
}
func (s *Stack[T]) Push(item T) {
s.mu.Lock()
defer s.mu.Unlock()
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.items) == 0 {
return s.zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
func (s *Stack[T]) Peek() (T, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
if len(s.items) == 0 {
return s.zero, false
}
return s.items[len(s.items)-1], true
}
func (s *Stack[T]) Len() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.items)
}
func (s *Stack[T]) Drain() []T {
s.mu.Lock()
defer s.mu.Unlock()
items := s.items
s.items = nil
return items
}
func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T {
s.mu.Lock()
defer s.mu.Unlock()
var items []T
for i := len(s.items) - 1; i >= 0; i-- {
if predicate(s.items[i]) {
items = append(items, s.items[i])
s.items = slices.Delete(s.items, i, i+1)
}
}
return items
}
// Copyright 2024 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 hashing provides common hashing utilities.
package hashing
import (
"crypto/md5"
"encoding/hex"
"io"
"strconv"
"sync"
"github.com/cespare/xxhash/v2"
"github.com/gohugoio/hashstructure"
"github.com/gohugoio/hugo/identity"
)
// XXHashFromReader calculates the xxHash for the given reader.
func XXHashFromReader(r io.Reader) (uint64, int64, error) {
h := getXxHashReadFrom()
defer putXxHashReadFrom(h)
size, err := io.Copy(h, r)
if err != nil {
return 0, 0, err
}
return h.Sum64(), size, nil
}
// XxHashFromReaderHexEncoded calculates the xxHash for the given reader
// and returns the hash as a hex encoded string.
func XxHashFromReaderHexEncoded(r io.Reader) (string, error) {
h := getXxHashReadFrom()
defer putXxHashReadFrom(h)
_, err := io.Copy(h, r)
if err != nil {
return "", err
}
hash := h.Sum(nil)
return hex.EncodeToString(hash), nil
}
// XXHashFromString calculates the xxHash for the given string.
func XXHashFromString(s string) (uint64, error) {
h := xxhash.New()
h.WriteString(s)
return h.Sum64(), nil
}
// XxHashFromStringHexEncoded calculates the xxHash for the given string
// and returns the hash as a hex encoded string.
func XxHashFromStringHexEncoded(f string) string {
h := xxhash.New()
h.WriteString(f)
hash := h.Sum(nil)
return hex.EncodeToString(hash)
}
// MD5FromStringHexEncoded returns the MD5 hash of the given string.
func MD5FromStringHexEncoded(f string) string {
h := md5.New()
h.Write([]byte(f))
return hex.EncodeToString(h.Sum(nil))
}
// HashString returns a hash from the given elements.
// It will panic if the hash cannot be calculated.
// Note that this hash should be used primarily for identity, not for change detection as
// it in the more complex values (e.g. Page) will not hash the full content.
func HashString(vs ...any) string {
hash := HashUint64(vs...)
return strconv.FormatUint(hash, 10)
}
// HashStringHex returns a hash from the given elements as a hex encoded string.
// See HashString for more information.
func HashStringHex(vs ...any) string {
hash := HashUint64(vs...)
return strconv.FormatUint(hash, 16)
}
var hashOptsPool = sync.Pool{
New: func() any {
return &hashstructure.HashOptions{
Hasher: xxhash.New(),
}
},
}
func getHashOpts() *hashstructure.HashOptions {
return hashOptsPool.Get().(*hashstructure.HashOptions)
}
func putHashOpts(opts *hashstructure.HashOptions) {
opts.Hasher.Reset()
hashOptsPool.Put(opts)
}
// HashUint64 returns a hash from the given elements.
// It will panic if the hash cannot be calculated.
// Note that this hash should be used primarily for identity, not for change detection as
// it in the more complex values (e.g. Page) will not hash the full content.
func HashUint64(vs ...any) uint64 {
var o any
if len(vs) == 1 {
o = toHashable(vs[0])
} else {
elements := make([]any, len(vs))
for i, e := range vs {
elements[i] = toHashable(e)
}
o = elements
}
hash, err := Hash(o)
if err != nil {
panic(err)
}
return hash
}
// Hash returns a hash from vs.
func Hash(vs ...any) (uint64, error) {
hashOpts := getHashOpts()
defer putHashOpts(hashOpts)
var v any = vs
if len(vs) == 1 {
v = vs[0]
}
return hashstructure.Hash(v, hashOpts)
}
type keyer interface {
Key() string
}
// For structs, hashstructure.Hash only works on the exported fields,
// so rewrite the input slice for known identity types.
func toHashable(v any) any {
switch t := v.(type) {
case keyer:
return t.Key()
case identity.IdentityProvider:
return t.GetIdentity()
default:
return v
}
}
type xxhashReadFrom struct {
buff []byte
*xxhash.Digest
}
func (x *xxhashReadFrom) ReadFrom(r io.Reader) (int64, error) {
for {
n, err := r.Read(x.buff)
if n > 0 {
x.Digest.Write(x.buff[:n])
}
if err != nil {
if err == io.EOF {
err = nil
}
return int64(n), err
}
}
}
var xXhashReadFromPool = sync.Pool{
New: func() any {
return &xxhashReadFrom{Digest: xxhash.New(), buff: make([]byte, 48*1024)}
},
}
func getXxHashReadFrom() *xxhashReadFrom {
return xXhashReadFromPool.Get().(*xxhashReadFrom)
}
func putXxHashReadFrom(h *xxhashReadFrom) {
h.Reset()
xXhashReadFromPool.Put(h)
}
// Copyright 2024 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 herrors contains common Hugo errors and error related utilities.
package herrors
import (
"io"
"path/filepath"
"strings"
"github.com/gohugoio/hugo/common/text"
)
// LineMatcher contains the elements used to match an error to a line
type LineMatcher struct {
Position text.Position
Error error
LineNumber int
Offset int
Line string
}
// LineMatcherFn is used to match a line with an error.
// It returns the column number or 0 if the line was found, but column could not be determined. Returns -1 if no line match.
type LineMatcherFn func(m LineMatcher) int
// SimpleLineMatcher simply matches by line number.
var SimpleLineMatcher = func(m LineMatcher) int {
if m.Position.LineNumber == m.LineNumber {
// We found the line, but don't know the column.
return 0
}
return -1
}
// NopLineMatcher is a matcher that always returns 1.
// This will effectively give line 1, column 1.
var NopLineMatcher = func(m LineMatcher) int {
return 1
}
// OffsetMatcher is a line matcher that matches by offset.
var OffsetMatcher = func(m LineMatcher) int {
if m.Offset+len(m.Line) >= m.Position.Offset {
// We found the line, but return 0 to signal that we want to determine
// the column from the error.
return 0
}
return -1
}
// ContainsMatcher is a line matcher that matches by line content.
func ContainsMatcher(text string) func(m LineMatcher) int {
return func(m LineMatcher) int {
if idx := strings.Index(m.Line, text); idx != -1 {
return idx + 1
}
return -1
}
}
// ErrorContext contains contextual information about an error. This will
// typically be the lines surrounding some problem in a file.
type ErrorContext struct {
// If a match will contain the matched line and up to 2 lines before and after.
// Will be empty if no match.
Lines []string
// The position of the error in the Lines above. 0 based.
LinesPos int
// The position of the content in the file. Note that this may be different from the error's position set
// in FileError.
Position text.Position
// The lexer to use for syntax highlighting.
// https://gohugo.io/content-management/syntax-highlighting/#list-of-chroma-highlighting-languages
ChromaLexer string
}
func chromaLexerFromType(fileType string) string {
switch fileType {
case "html", "htm":
return "go-html-template"
}
return fileType
}
func extNoDelimiter(filename string) string {
return strings.TrimPrefix(filepath.Ext(filename), ".")
}
func chromaLexerFromFilename(filename string) string {
if strings.Contains(filename, "layouts") {
return "go-html-template"
}
ext := extNoDelimiter(filename)
return chromaLexerFromType(ext)
}
func locateErrorInString(src string, matcher LineMatcherFn) *ErrorContext {
return locateError(strings.NewReader(src), &fileError{}, matcher)
}
func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext {
if le == nil {
panic("must provide an error")
}
ectx := &ErrorContext{LinesPos: -1, Position: text.Position{Offset: -1}}
b, err := io.ReadAll(r)
if err != nil {
return ectx
}
lines := strings.Split(string(b), "\n")
lineNo := 0
posBytes := 0
for li, line := range lines {
lineNo = li + 1
m := LineMatcher{
Position: le.Position(),
Error: le,
LineNumber: lineNo,
Offset: posBytes,
Line: line,
}
v := matches(m)
if ectx.LinesPos == -1 && v != -1 {
ectx.Position.LineNumber = lineNo
ectx.Position.ColumnNumber = v
break
}
posBytes += len(line)
}
if ectx.Position.LineNumber > 0 {
low := max(ectx.Position.LineNumber-3, 0)
if ectx.Position.LineNumber > 2 {
ectx.LinesPos = 2
} else {
ectx.LinesPos = ectx.Position.LineNumber - 1
}
high := min(ectx.Position.LineNumber+2, len(lines))
ectx.Lines = lines[low:high]
}
return ectx
}
// Copyright 2024 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 herrors contains common Hugo errors and error related utilities.
package herrors
import (
"errors"
"fmt"
"io"
"os"
"regexp"
"runtime"
"runtime/debug"
"strings"
"time"
)
// PrintStackTrace prints the current stacktrace to w.
func PrintStackTrace(w io.Writer) {
buf := make([]byte, 1<<16)
runtime.Stack(buf, true)
fmt.Fprintf(w, "%s", buf)
}
// ErrorSender is a, typically, non-blocking error handler.
type ErrorSender interface {
SendError(err error)
}
// Recover is a helper function that can be used to capture panics.
// Put this at the top of a method/function that crashes in a template:
//
// defer herrors.Recover()
func Recover(args ...any) {
if r := recover(); r != nil {
fmt.Println("ERR:", r)
args = append(args, "stacktrace from panic: \n"+string(debug.Stack()), "\n")
fmt.Println(args...)
}
}
// IsTimeoutError returns true if the given error is or contains a TimeoutError.
func IsTimeoutError(err error) bool {
return errors.Is(err, &TimeoutError{})
}
type TimeoutError struct {
Duration time.Duration
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("timeout after %s", e.Duration)
}
func (e *TimeoutError) Is(target error) bool {
_, ok := target.(*TimeoutError)
return ok
}
// errMessage wraps an error with a message.
type errMessage struct {
msg string
err error
}
func (e *errMessage) Error() string {
return e.msg
}
func (e *errMessage) Unwrap() error {
return e.err
}
// IsFeatureNotAvailableError returns true if the given error is or contains a FeatureNotAvailableError.
func IsFeatureNotAvailableError(err error) bool {
return errors.Is(err, &FeatureNotAvailableError{})
}
// ErrFeatureNotAvailable denotes that a feature is unavailable.
//
// We will, at least to begin with, make some Hugo features (SCSS with libsass) optional,
// and this error is used to signal those situations.
var ErrFeatureNotAvailable = &FeatureNotAvailableError{Cause: errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")}
// FeatureNotAvailableError is an error type used to signal that a feature is not available.
type FeatureNotAvailableError struct {
Cause error
}
func (e *FeatureNotAvailableError) Unwrap() error {
return e.Cause
}
func (e *FeatureNotAvailableError) Error() string {
return e.Cause.Error()
}
func (e *FeatureNotAvailableError) Is(target error) bool {
_, ok := target.(*FeatureNotAvailableError)
return ok
}
// Must panics if err != nil.
func Must(err error) {
if err != nil {
panic(err)
}
}
// IsNotExist returns true if the error is a file not found error.
// Unlike os.IsNotExist, this also considers wrapped errors.
func IsNotExist(err error) bool {
if os.IsNotExist(err) {
return true
}
// os.IsNotExist does not consider wrapped errors.
if os.IsNotExist(errors.Unwrap(err)) {
return true
}
return false
}
// IsExist returns true if the error is a file exists error.
// Unlike os.IsExist, this also considers wrapped errors.
func IsExist(err error) bool {
if os.IsExist(err) {
return true
}
// os.IsExist does not consider wrapped errors.
if os.IsExist(errors.Unwrap(err)) {
return true
}
return false
}
var nilPointerErrRe = regexp.MustCompile(`at <(.*)>: error calling (.*?): runtime error: invalid memory address or nil pointer dereference`)
const deferredPrefix = "__hdeferred/"
var deferredStringToRemove = regexp.MustCompile(`executing "__hdeferred/.*?" `)
// ImproveRenderErr improves the error message for rendering errors.
func ImproveRenderErr(inErr error) (outErr error) {
outErr = inErr
msg := improveIfNilPointerMsg(inErr)
if msg != "" {
outErr = &errMessage{msg: msg, err: outErr}
}
if strings.Contains(inErr.Error(), deferredPrefix) {
msg := deferredStringToRemove.ReplaceAllString(inErr.Error(), "executing ")
outErr = &errMessage{msg: msg, err: outErr}
}
return
}
func improveIfNilPointerMsg(inErr error) string {
m := nilPointerErrRe.FindStringSubmatch(inErr.Error())
if len(m) == 0 {
return ""
}
call := m[1]
field := m[2]
parts := strings.Split(call, ".")
if len(parts) < 2 {
return ""
}
receiverName := parts[len(parts)-2]
receiver := strings.Join(parts[:len(parts)-1], ".")
s := fmt.Sprintf("– %s is nil; wrap it in if or with: {{ with %s }}{{ .%s }}{{ end }}", receiverName, receiver, field)
return nilPointerErrRe.ReplaceAllString(inErr.Error(), s)
}
// Copyright 2024 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 lfmtaw 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 herrors
import (
"encoding/json"
"errors"
"fmt"
"io"
"path/filepath"
"github.com/bep/godartsass/v2"
"github.com/bep/golibsass/libsass/libsasserrors"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/text"
"github.com/pelletier/go-toml/v2"
"github.com/spf13/afero"
"github.com/tdewolff/parse/v2"
)
// FileError represents an error when handling a file: Parsing a config file,
// execute a template etc.
type FileError interface {
error
// ErrorContext holds some context information about the error.
ErrorContext() *ErrorContext
text.Positioner
// UpdatePosition updates the position of the error.
UpdatePosition(pos text.Position) FileError
// UpdateContent updates the error with a new ErrorContext from the content of the file.
UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError
// SetFilename sets the filename of the error.
SetFilename(filename string) FileError
}
// Unwrapper can unwrap errors created with fmt.Errorf.
type Unwrapper interface {
Unwrap() error
}
var (
_ FileError = (*fileError)(nil)
_ Unwrapper = (*fileError)(nil)
)
func (fe *fileError) SetFilename(filename string) FileError {
fe.position.Filename = filename
return fe
}
func (fe *fileError) UpdatePosition(pos text.Position) FileError {
oldFilename := fe.Position().Filename
if pos.Filename != "" && fe.fileType == "" {
_, fe.fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename))
}
if pos.Filename == "" {
pos.Filename = oldFilename
}
fe.position = pos
return fe
}
func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError {
if linematcher == nil {
linematcher = SimpleLineMatcher
}
var (
posle = fe.position
ectx *ErrorContext
)
if posle.LineNumber <= 1 && posle.Offset > 0 {
// Try to locate the line number from the content if offset is set.
ectx = locateError(r, fe, func(m LineMatcher) int {
if posle.Offset >= m.Offset && posle.Offset < m.Offset+len(m.Line) {
lno := posle.LineNumber - m.Position.LineNumber + m.LineNumber
m.Position = text.Position{LineNumber: lno}
return linematcher(m)
}
return -1
})
} else {
ectx = locateError(r, fe, linematcher)
}
if ectx.ChromaLexer == "" {
if fe.fileType != "" {
ectx.ChromaLexer = chromaLexerFromType(fe.fileType)
} else {
ectx.ChromaLexer = chromaLexerFromFilename(fe.Position().Filename)
}
}
fe.errorContext = ectx
if ectx.Position.LineNumber > 0 {
fe.position.LineNumber = ectx.Position.LineNumber
}
if ectx.Position.ColumnNumber > 0 {
fe.position.ColumnNumber = ectx.Position.ColumnNumber
}
return fe
}
type fileError struct {
position text.Position
errorContext *ErrorContext
fileType string
cause error
}
func (e *fileError) ErrorContext() *ErrorContext {
return e.errorContext
}
// Position returns the text position of this error.
func (e fileError) Position() text.Position {
return e.position
}
func (e *fileError) Error() string {
return fmt.Sprintf("%s: %s", e.position, e.causeString())
}
func (e *fileError) causeString() string {
if e.cause == nil {
return ""
}
switch v := e.cause.(type) {
// Avoid repeating the file info in the error message.
case godartsass.SassError:
return v.Message
case libsasserrors.Error:
return v.Message
default:
return v.Error()
}
}
func (e *fileError) Unwrap() error {
return e.cause
}
// NewFileError creates a new FileError that wraps err.
// It will try to extract the filename and line number from err.
func NewFileError(err error) FileError {
// Filetype is used to determine the Chroma lexer to use.
fileType, pos := extractFileTypePos(err)
return &fileError{cause: err, fileType: fileType, position: pos}
}
// NewFileErrorFromName creates a new FileError that wraps err.
// The value for name should identify the file, the best
// being the full filename to the file on disk.
func NewFileErrorFromName(err error, name string) FileError {
// Filetype is used to determine the Chroma lexer to use.
fileType, pos := extractFileTypePos(err)
pos.Filename = name
if fileType == "" {
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name))
}
return &fileError{cause: err, fileType: fileType, position: pos}
}
// NewFileErrorFromPos will use the filename and line number from pos to create a new FileError, wrapping err.
func NewFileErrorFromPos(err error, pos text.Position) FileError {
// Filetype is used to determine the Chroma lexer to use.
fileType, _ := extractFileTypePos(err)
if fileType == "" {
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename))
}
return &fileError{cause: err, fileType: fileType, position: pos}
}
func NewFileErrorFromFileInErr(err error, fs afero.Fs, linematcher LineMatcherFn) FileError {
fe := NewFileError(err)
pos := fe.Position()
if pos.Filename == "" {
return fe
}
f, realFilename, err2 := openFile(pos.Filename, fs)
if err2 != nil {
return fe
}
pos.Filename = realFilename
defer f.Close()
return fe.UpdateContent(f, linematcher)
}
func NewFileErrorFromFileInPos(err error, pos text.Position, fs afero.Fs, linematcher LineMatcherFn) FileError {
if err == nil {
panic("err is nil")
}
f, realFilename, err2 := openFile(pos.Filename, fs)
if err2 != nil {
return NewFileErrorFromPos(err, pos)
}
pos.Filename = realFilename
defer f.Close()
return NewFileErrorFromPos(err, pos).UpdateContent(f, linematcher)
}
// NewFileErrorFromFile is a convenience method to create a new FileError from a file.
func NewFileErrorFromFile(err error, filename string, fs afero.Fs, linematcher LineMatcherFn) FileError {
if err == nil {
panic("err is nil")
}
f, realFilename, err2 := openFile(filename, fs)
if err2 != nil {
return NewFileErrorFromName(err, realFilename)
}
defer f.Close()
return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher)
}
func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
realFilename := filename
// We want the most specific filename possible in the error message.
fi, err2 := fs.Stat(filename)
if err2 == nil {
if s, ok := fi.(interface {
Filename() string
}); ok {
realFilename = s.Filename()
}
}
f, err2 := fs.Open(filename)
if err2 != nil {
return nil, realFilename, err2
}
return f, realFilename, nil
}
// Cause returns the underlying error, that is,
// it unwraps errors until it finds one that does not implement
// the Unwrap method.
// For a shallow variant, see Unwrap.
func Cause(err error) error {
type unwrapper interface {
Unwrap() error
}
for err != nil {
cause, ok := err.(unwrapper)
if !ok {
break
}
err = cause.Unwrap()
}
return err
}
// Unwrap returns the underlying error or itself if it does not implement Unwrap.
func Unwrap(err error) error {
if u := errors.Unwrap(err); u != nil {
return u
}
return err
}
func extractFileTypePos(err error) (string, text.Position) {
err = Unwrap(err)
var fileType string
// LibSass, DartSass
if pos := extractPosition(err); pos.LineNumber > 0 || pos.Offset > 0 {
_, fileType = paths.FileAndExtNoDelimiter(pos.Filename)
return fileType, pos
}
// Default to line 1 col 1 if we don't find any better.
pos := text.Position{
Offset: -1,
LineNumber: 1,
ColumnNumber: 1,
}
// JSON errors.
offset, typ := extractOffsetAndType(err)
if fileType == "" {
fileType = typ
}
if offset >= 0 {
pos.Offset = offset
}
// The error type from the minifier contains line number and column number.
if line, col := extractLineNumberAndColumnNumber(err); line >= 0 {
pos.LineNumber = line
pos.ColumnNumber = col
return fileType, pos
}
// Look in the error message for the line number.
for _, handle := range lineNumberExtractors {
lno, col := handle(err)
if lno > 0 {
pos.ColumnNumber = col
pos.LineNumber = lno
break
}
}
if fileType == "" && pos.Filename != "" {
_, fileType = paths.FileAndExtNoDelimiter(pos.Filename)
}
return fileType, pos
}
// UnwrapFileError tries to unwrap a FileError from err.
// It returns nil if this is not possible.
func UnwrapFileError(err error) FileError {
for err != nil {
switch v := err.(type) {
case FileError:
return v
default:
err = errors.Unwrap(err)
}
}
return nil
}
// UnwrapFileErrors tries to unwrap all FileError.
func UnwrapFileErrors(err error) []FileError {
var errs []FileError
for err != nil {
if v, ok := err.(FileError); ok {
errs = append(errs, v)
}
err = errors.Unwrap(err)
}
return errs
}
// UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext.
func UnwrapFileErrorsWithErrorContext(err error) []FileError {
var errs []FileError
for err != nil {
if v, ok := err.(FileError); ok && v.ErrorContext() != nil {
errs = append(errs, v)
}
err = errors.Unwrap(err)
}
return errs
}
func extractOffsetAndType(e error) (int, string) {
switch v := e.(type) {
case *json.UnmarshalTypeError:
return int(v.Offset), "json"
case *json.SyntaxError:
return int(v.Offset), "json"
default:
return -1, ""
}
}
func extractLineNumberAndColumnNumber(e error) (int, int) {
switch v := e.(type) {
case *parse.Error:
return v.Line, v.Column
case *toml.DecodeError:
return v.Position()
}
return -1, -1
}
func extractPosition(e error) (pos text.Position) {
switch v := e.(type) {
case godartsass.SassError:
span := v.Span
start := span.Start
filename, _ := paths.UrlStringToFilename(span.Url)
pos.Filename = filename
pos.Offset = start.Offset
pos.ColumnNumber = start.Column
case libsasserrors.Error:
pos.Filename = v.File
pos.LineNumber = v.Line
pos.ColumnNumber = v.Column
}
return
}
// TextSegmentError is an error with a text segment attached.
type TextSegmentError struct {
Segment string
Err error
}
func (e TextSegmentError) Unwrap() error {
return e.Err
}
func (e TextSegmentError) Error() string {
return e.Err.Error()
}
// Copyright 2018 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 herrors
import (
"regexp"
"strconv"
)
var lineNumberExtractors = []lineNumberExtractor{
// Template/shortcode parse errors
newLineNumberErrHandlerFromRegexp(`:(\d+):(\d*):`),
newLineNumberErrHandlerFromRegexp(`:(\d+):`),
// YAML parse errors
newLineNumberErrHandlerFromRegexp(`line (\d+):`),
// i18n bundle errors
newLineNumberErrHandlerFromRegexp(`\((\d+),\s(\d*)`),
}
type lineNumberExtractor func(e error) (int, int)
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
re := regexp.MustCompile(expression)
return extractLineNo(re)
}
func extractLineNo(re *regexp.Regexp) lineNumberExtractor {
return func(e error) (int, int) {
if e == nil {
panic("no error")
}
col := 1
s := e.Error()
m := re.FindStringSubmatch(s)
if len(m) >= 2 {
lno, _ := strconv.Atoi(m[1])
if len(m) > 2 {
col, _ = strconv.Atoi(m[2])
}
if col <= 0 {
col = 1
}
return lno, col
}
return 0, col
}
}
// Copyright 2024 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 maps
import (
"sync"
)
// Cache is a simple thread safe cache backed by a map.
type Cache[K comparable, T any] struct {
m map[K]T
hasBeenInitialized bool
sync.RWMutex
}
// NewCache creates a new Cache.
func NewCache[K comparable, T any]() *Cache[K, T] {
return &Cache[K, T]{m: make(map[K]T)}
}
// Delete deletes the given key from the cache.
// If c is nil, this method is a no-op.
func (c *Cache[K, T]) Get(key K) (T, bool) {
if c == nil {
var zero T
return zero, false
}
c.RLock()
v, found := c.get(key)
c.RUnlock()
return v, found
}
func (c *Cache[K, T]) get(key K) (T, bool) {
v, found := c.m[key]
return v, found
}
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
c.RLock()
v, found := c.m[key]
c.RUnlock()
if found {
return v, nil
}
c.Lock()
defer c.Unlock()
v, found = c.m[key]
if found {
return v, nil
}
v, err := create()
if err != nil {
return v, err
}
c.m[key] = v
return v, nil
}
// Contains returns whether the given key exists in the cache.
func (c *Cache[K, T]) Contains(key K) bool {
c.RLock()
_, found := c.m[key]
c.RUnlock()
return found
}
// InitAndGet initializes the cache if not already done and returns the value for the given key.
// The init state will be reset on Reset or Drain.
func (c *Cache[K, T]) InitAndGet(key K, init func(get func(key K) (T, bool), set func(key K, value T)) error) (T, error) {
var v T
c.RLock()
if !c.hasBeenInitialized {
c.RUnlock()
if err := func() error {
c.Lock()
defer c.Unlock()
// Double check in case another goroutine has initialized it in the meantime.
if !c.hasBeenInitialized {
err := init(c.get, c.set)
if err != nil {
return err
}
c.hasBeenInitialized = true
}
return nil
}(); err != nil {
return v, err
}
// Reacquire the read lock.
c.RLock()
}
v = c.m[key]
c.RUnlock()
return v, nil
}
// Set sets the given key to the given value.
func (c *Cache[K, T]) Set(key K, value T) {
c.Lock()
c.set(key, value)
c.Unlock()
}
// SetIfAbsent sets the given key to the given value if the key does not already exist in the cache.
func (c *Cache[K, T]) SetIfAbsent(key K, value T) {
c.RLock()
if _, found := c.get(key); !found {
c.RUnlock()
c.Set(key, value)
} else {
c.RUnlock()
}
}
func (c *Cache[K, T]) set(key K, value T) {
c.m[key] = value
}
// ForEeach calls the given function for each key/value pair in the cache.
// If the function returns false, the iteration stops.
func (c *Cache[K, T]) ForEeach(f func(K, T) bool) {
c.RLock()
defer c.RUnlock()
for k, v := range c.m {
if !f(k, v) {
return
}
}
}
func (c *Cache[K, T]) Drain() map[K]T {
c.Lock()
m := c.m
c.m = make(map[K]T)
c.hasBeenInitialized = false
c.Unlock()
return m
}
func (c *Cache[K, T]) Len() int {
c.RLock()
defer c.RUnlock()
return len(c.m)
}
func (c *Cache[K, T]) Reset() {
c.Lock()
clear(c.m)
c.hasBeenInitialized = false
c.Unlock()
}
// SliceCache is a simple thread safe cache backed by a map.
type SliceCache[T any] struct {
m map[string][]T
sync.RWMutex
}
func NewSliceCache[T any]() *SliceCache[T] {
return &SliceCache[T]{m: make(map[string][]T)}
}
func (c *SliceCache[T]) Get(key string) ([]T, bool) {
c.RLock()
v, found := c.m[key]
c.RUnlock()
return v, found
}
func (c *SliceCache[T]) Append(key string, values ...T) {
c.Lock()
c.m[key] = append(c.m[key], values...)
c.Unlock()
}
func (c *SliceCache[T]) Reset() {
c.Lock()
c.m = make(map[string][]T)
c.Unlock()
}
// Copyright 2018 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 maps
import (
"fmt"
"strings"
"github.com/gohugoio/hugo/common/types"
"github.com/gobwas/glob"
"github.com/spf13/cast"
)
// ToStringMapE converts in to map[string]interface{}.
func ToStringMapE(in any) (map[string]any, error) {
switch vv := in.(type) {
case Params:
return vv, nil
case map[string]string:
m := map[string]any{}
for k, v := range vv {
m[k] = v
}
return m, nil
default:
return cast.ToStringMapE(in)
}
}
// ToParamsAndPrepare converts in to Params and prepares it for use.
// If in is nil, an empty map is returned.
// See PrepareParams.
func ToParamsAndPrepare(in any) (Params, error) {
if types.IsNil(in) {
return Params{}, nil
}
m, err := ToStringMapE(in)
if err != nil {
return nil, err
}
PrepareParams(m)
return m, nil
}
// MustToParamsAndPrepare calls ToParamsAndPrepare and panics if it fails.
func MustToParamsAndPrepare(in any) Params {
p, err := ToParamsAndPrepare(in)
if err != nil {
panic(fmt.Sprintf("cannot convert %T to maps.Params: %s", in, err))
}
return p
}
// ToStringMap converts in to map[string]interface{}.
func ToStringMap(in any) map[string]any {
m, _ := ToStringMapE(in)
return m
}
// ToStringMapStringE converts in to map[string]string.
func ToStringMapStringE(in any) (map[string]string, error) {
m, err := ToStringMapE(in)
if err != nil {
return nil, err
}
return cast.ToStringMapStringE(m)
}
// ToStringMapString converts in to map[string]string.
func ToStringMapString(in any) map[string]string {
m, _ := ToStringMapStringE(in)
return m
}
// ToStringMapBool converts in to bool.
func ToStringMapBool(in any) map[string]bool {
m, _ := ToStringMapE(in)
return cast.ToStringMapBool(m)
}
// ToSliceStringMap converts in to []map[string]interface{}.
func ToSliceStringMap(in any) ([]map[string]any, error) {
switch v := in.(type) {
case []map[string]any:
return v, nil
case Params:
return []map[string]any{v}, nil
case []any:
var s []map[string]any
for _, entry := range v {
if vv, ok := entry.(map[string]any); ok {
s = append(s, vv)
}
}
return s, nil
default:
return nil, fmt.Errorf("unable to cast %#v of type %T to []map[string]interface{}", in, in)
}
}
// LookupEqualFold finds key in m with case insensitive equality checks.
func LookupEqualFold[T any | string](m map[string]T, key string) (T, string, bool) {
if v, found := m[key]; found {
return v, key, true
}
for k, v := range m {
if strings.EqualFold(k, key) {
return v, k, true
}
}
var s T
return s, "", false
}
// MergeShallow merges src into dst, but only if the key does not already exist in dst.
// The keys are compared case insensitively.
func MergeShallow(dst, src map[string]any) {
for k, v := range src {
found := false
for dk := range dst {
if strings.EqualFold(dk, k) {
found = true
break
}
}
if !found {
dst[k] = v
}
}
}
type keyRename struct {
pattern glob.Glob
newKey string
}
// KeyRenamer supports renaming of keys in a map.
type KeyRenamer struct {
renames []keyRename
}
// NewKeyRenamer creates a new KeyRenamer given a list of pattern and new key
// value pairs.
func NewKeyRenamer(patternKeys ...string) (KeyRenamer, error) {
var renames []keyRename
for i := 0; i < len(patternKeys); i += 2 {
g, err := glob.Compile(strings.ToLower(patternKeys[i]), '/')
if err != nil {
return KeyRenamer{}, err
}
renames = append(renames, keyRename{pattern: g, newKey: patternKeys[i+1]})
}
return KeyRenamer{renames: renames}, nil
}
func (r KeyRenamer) getNewKey(keyPath string) string {
for _, matcher := range r.renames {
if matcher.pattern.Match(keyPath) {
return matcher.newKey
}
}
return ""
}
// Rename renames the keys in the given map according
// to the patterns in the current KeyRenamer.
func (r KeyRenamer) Rename(m map[string]any) {
r.renamePath("", m)
}
func (KeyRenamer) keyPath(k1, k2 string) string {
k1, k2 = strings.ToLower(k1), strings.ToLower(k2)
if k1 == "" {
return k2
}
return k1 + "/" + k2
}
func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]any) {
for k, v := range m {
keyPath := r.keyPath(parentKeyPath, k)
switch vv := v.(type) {
case map[any]any:
r.renamePath(keyPath, cast.ToStringMap(vv))
case map[string]any:
r.renamePath(keyPath, vv)
}
newKey := r.getNewKey(keyPath)
if newKey != "" {
delete(m, k)
m[newKey] = v
}
}
}
// ConvertFloat64WithNoDecimalsToInt converts float64 values with no decimals to int recursively.
func ConvertFloat64WithNoDecimalsToInt(m map[string]any) {
for k, v := range m {
switch vv := v.(type) {
case float64:
if v == float64(int64(vv)) {
m[k] = int64(vv)
}
case map[string]any:
ConvertFloat64WithNoDecimalsToInt(vv)
case []any:
for i, vvv := range vv {
switch vvvv := vvv.(type) {
case float64:
if vvv == float64(int64(vvvv)) {
vv[i] = int64(vvvv)
}
case map[string]any:
ConvertFloat64WithNoDecimalsToInt(vvvv)
}
}
}
}
}
// Copyright 2024 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 maps
import (
"slices"
"github.com/gohugoio/hugo/common/hashing"
)
// Ordered is a map that can be iterated in the order of insertion.
// Note that insertion order is not affected if a key is re-inserted into the map.
// In a nil map, all operations are no-ops.
// This is not thread safe.
type Ordered[K comparable, T any] struct {
// The keys in the order they were added.
keys []K
// The values.
values map[K]T
}
// NewOrdered creates a new Ordered map.
func NewOrdered[K comparable, T any]() *Ordered[K, T] {
return &Ordered[K, T]{values: make(map[K]T)}
}
// Set sets the value for the given key.
// Note that insertion order is not affected if a key is re-inserted into the map.
func (m *Ordered[K, T]) Set(key K, value T) {
if m == nil {
return
}
// Check if key already exists.
if _, found := m.values[key]; !found {
m.keys = append(m.keys, key)
}
m.values[key] = value
}
// Get gets the value for the given key.
func (m *Ordered[K, T]) Get(key K) (T, bool) {
if m == nil {
var v T
return v, false
}
value, found := m.values[key]
return value, found
}
// Has returns whether the given key exists in the map.
func (m *Ordered[K, T]) Has(key K) bool {
if m == nil {
return false
}
_, found := m.values[key]
return found
}
// Delete deletes the value for the given key.
func (m *Ordered[K, T]) Delete(key K) {
if m == nil {
return
}
delete(m.values, key)
for i, k := range m.keys {
if k == key {
m.keys = slices.Delete(m.keys, i, i+1)
break
}
}
}
// Clone creates a shallow copy of the map.
func (m *Ordered[K, T]) Clone() *Ordered[K, T] {
if m == nil {
return nil
}
clone := NewOrdered[K, T]()
for _, k := range m.keys {
clone.Set(k, m.values[k])
}
return clone
}
// Keys returns the keys in the order they were added.
func (m *Ordered[K, T]) Keys() []K {
if m == nil {
return nil
}
return m.keys
}
// Values returns the values in the order they were added.
func (m *Ordered[K, T]) Values() []T {
if m == nil {
return nil
}
var values []T
for _, k := range m.keys {
values = append(values, m.values[k])
}
return values
}
// Len returns the number of items in the map.
func (m *Ordered[K, T]) Len() int {
if m == nil {
return 0
}
return len(m.keys)
}
// Range calls f sequentially for each key and value present in the map.
// If f returns false, range stops the iteration.
// TODO(bep) replace with iter.Seq2 when we bump go Go 1.24.
func (m *Ordered[K, T]) Range(f func(key K, value T) bool) {
if m == nil {
return
}
for _, k := range m.keys {
if !f(k, m.values[k]) {
return
}
}
}
// Hash calculates a hash from the values.
func (m *Ordered[K, T]) Hash() (uint64, error) {
if m == nil {
return 0, nil
}
return hashing.Hash(m.values)
}
// Copyright 2019 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 maps
import (
"fmt"
"strings"
"github.com/spf13/cast"
)
// Params is a map where all keys are lower case.
type Params map[string]any
// KeyParams is an utility struct for the WalkParams method.
type KeyParams struct {
Key string
Params Params
}
// GetNested does a lower case and nested search in this map.
// It will return nil if none found.
// Make all of these methods internal somehow.
func (p Params) GetNested(indices ...string) any {
v, _, _ := getNested(p, indices)
return v
}
// SetParams overwrites values in dst with values in src for common or new keys.
// This is done recursively.
func SetParams(dst, src Params) {
for k, v := range src {
vv, found := dst[k]
if !found {
dst[k] = v
} else {
switch vvv := vv.(type) {
case Params:
if pv, ok := v.(Params); ok {
SetParams(vvv, pv)
} else {
dst[k] = v
}
default:
dst[k] = v
}
}
}
}
// IsZero returns true if p is considered empty.
func (p Params) IsZero() bool {
if len(p) == 0 {
return true
}
if len(p) > 1 {
return false
}
for k := range p {
return k == MergeStrategyKey
}
return false
}
// MergeParamsWithStrategy transfers values from src to dst for new keys using the merge strategy given.
// This is done recursively.
func MergeParamsWithStrategy(strategy string, dst, src Params) {
dst.merge(ParamsMergeStrategy(strategy), src)
}
// MergeParams transfers values from src to dst for new keys using the merge encoded in dst.
// This is done recursively.
func MergeParams(dst, src Params) {
ms, _ := dst.GetMergeStrategy()
dst.merge(ms, src)
}
func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
ns, found := p.GetMergeStrategy()
ms := ns
if !found && ps != "" {
ms = ps
}
noUpdate := ms == ParamsMergeStrategyNone
noUpdate = noUpdate || (ps != "" && ps == ParamsMergeStrategyShallow)
for k, v := range pp {
if k == MergeStrategyKey {
continue
}
vv, found := p[k]
if found {
// Key matches, if both sides are Params, we try to merge.
if vvv, ok := vv.(Params); ok {
if pv, ok := v.(Params); ok {
vvv.merge(ms, pv)
}
}
} else if !noUpdate {
p[k] = v
}
}
}
// For internal use.
func (p Params) GetMergeStrategy() (ParamsMergeStrategy, bool) {
if v, found := p[MergeStrategyKey]; found {
if s, ok := v.(ParamsMergeStrategy); ok {
return s, true
}
}
return ParamsMergeStrategyShallow, false
}
// For internal use.
func (p Params) DeleteMergeStrategy() bool {
if _, found := p[MergeStrategyKey]; found {
delete(p, MergeStrategyKey)
return true
}
return false
}
// For internal use.
func (p Params) SetMergeStrategy(s ParamsMergeStrategy) {
switch s {
case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow:
default:
panic(fmt.Sprintf("invalid merge strategy %q", s))
}
p[MergeStrategyKey] = s
}
func getNested(m map[string]any, indices []string) (any, string, map[string]any) {
if len(indices) == 0 {
return nil, "", nil
}
first := indices[0]
v, found := m[strings.ToLower(cast.ToString(first))]
if !found {
if len(indices) == 1 {
return nil, first, m
}
return nil, "", nil
}
if len(indices) == 1 {
return v, first, m
}
switch m2 := v.(type) {
case Params:
return getNested(m2, indices[1:])
case map[string]any:
return getNested(m2, indices[1:])
default:
return nil, "", nil
}
}
// GetNestedParam gets the first match of the keyStr in the candidates given.
// It will first try the exact match and then try to find it as a nested map value,
// using the given separator, e.g. "mymap.name".
// It assumes that all the maps given have lower cased keys.
func GetNestedParam(keyStr, separator string, candidates ...Params) (any, error) {
keyStr = strings.ToLower(keyStr)
// Try exact match first
for _, m := range candidates {
if v, ok := m[keyStr]; ok {
return v, nil
}
}
keySegments := strings.Split(keyStr, separator)
for _, m := range candidates {
if v := m.GetNested(keySegments...); v != nil {
return v, nil
}
}
return nil, nil
}
func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) any) (any, string, map[string]any, error) {
keySegments := strings.Split(keyStr, separator)
if len(keySegments) == 0 {
return nil, "", nil, nil
}
first := lookupFn(keySegments[0])
if first == nil {
return nil, "", nil, nil
}
if len(keySegments) == 1 {
return first, keySegments[0], nil, nil
}
switch m := first.(type) {
case map[string]any:
v, key, owner := getNested(m, keySegments[1:])
return v, key, owner, nil
case Params:
v, key, owner := getNested(m, keySegments[1:])
return v, key, owner, nil
}
return nil, "", nil, nil
}
// ParamsMergeStrategy tells what strategy to use in Params.Merge.
type ParamsMergeStrategy string
const (
// Do not merge.
ParamsMergeStrategyNone ParamsMergeStrategy = "none"
// Only add new keys.
ParamsMergeStrategyShallow ParamsMergeStrategy = "shallow"
// Add new keys, merge existing.
ParamsMergeStrategyDeep ParamsMergeStrategy = "deep"
MergeStrategyKey = "_merge"
)
// CleanConfigStringMapString removes any processing instructions from m,
// m will never be modified.
func CleanConfigStringMapString(m map[string]string) map[string]string {
if len(m) == 0 {
return m
}
if _, found := m[MergeStrategyKey]; !found {
return m
}
// Create a new map and copy all the keys except the merge strategy key.
m2 := make(map[string]string, len(m)-1)
for k, v := range m {
if k != MergeStrategyKey {
m2[k] = v
}
}
return m2
}
// CleanConfigStringMap is the same as CleanConfigStringMapString but for
// map[string]any.
func CleanConfigStringMap(m map[string]any) map[string]any {
if len(m) == 0 {
return m
}
if _, found := m[MergeStrategyKey]; !found {
return m
}
// Create a new map and copy all the keys except the merge strategy key.
m2 := make(map[string]any, len(m)-1)
for k, v := range m {
if k != MergeStrategyKey {
m2[k] = v
}
switch v2 := v.(type) {
case map[string]any:
m2[k] = CleanConfigStringMap(v2)
case Params:
var p Params = CleanConfigStringMap(v2)
m2[k] = p
case map[string]string:
m2[k] = CleanConfigStringMapString(v2)
}
}
return m2
}
func toMergeStrategy(v any) ParamsMergeStrategy {
s := ParamsMergeStrategy(cast.ToString(v))
switch s {
case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow:
return s
default:
return ParamsMergeStrategyDeep
}
}
// PrepareParams
// * makes all the keys in the given map lower cased and will do so recursively.
// * This will modify the map given.
// * Any nested map[interface{}]interface{}, map[string]interface{},map[string]string will be converted to Params.
// * Any _merge value will be converted to proper type and value.
func PrepareParams(m Params) {
for k, v := range m {
var retyped bool
lKey := strings.ToLower(k)
if lKey == MergeStrategyKey {
v = toMergeStrategy(v)
retyped = true
} else {
switch vv := v.(type) {
case map[any]any:
var p Params = cast.ToStringMap(v)
v = p
PrepareParams(p)
retyped = true
case map[string]any:
var p Params = v.(map[string]any)
v = p
PrepareParams(p)
retyped = true
case map[string]string:
p := make(Params)
for k, v := range vv {
p[k] = v
}
v = p
PrepareParams(p)
retyped = true
}
}
if retyped || k != lKey {
delete(m, k)
m[lKey] = v
}
}
}
// PrepareParamsClone is like PrepareParams, but it does not modify the input.
func PrepareParamsClone(m Params) Params {
m2 := make(Params)
for k, v := range m {
var retyped bool
lKey := strings.ToLower(k)
if lKey == MergeStrategyKey {
v = toMergeStrategy(v)
retyped = true
} else {
switch vv := v.(type) {
case map[any]any:
var p Params = cast.ToStringMap(v)
v = PrepareParamsClone(p)
retyped = true
case map[string]any:
var p Params = v.(map[string]any)
v = PrepareParamsClone(p)
retyped = true
case map[string]string:
p := make(Params)
for k, v := range vv {
p[k] = v
}
v = p
PrepareParams(p)
retyped = true
}
}
if retyped || k != lKey {
m2[lKey] = v
} else {
m2[k] = v
}
}
return m2
}
// Copyright 2019 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 maps
import (
"reflect"
"sort"
"sync"
"github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/math"
)
type StoreProvider interface {
// Store returns a Scratch that can be used to store temporary state.
// Store is not reset on server rebuilds.
Store() *Scratch
}
// Scratch is a writable context used for stateful build operations
type Scratch struct {
values map[string]any
mu sync.RWMutex
}
// Add will, for single values, add (using the + operator) the addend to the existing addend (if found).
// Supports numeric values and strings.
//
// If the first add for a key is an array or slice, then the next value(s) will be appended.
func (c *Scratch) Add(key string, newAddend any) (string, error) {
var newVal any
c.mu.RLock()
existingAddend, found := c.values[key]
c.mu.RUnlock()
if found {
var err error
addendV := reflect.TypeOf(existingAddend)
if addendV.Kind() == reflect.Slice || addendV.Kind() == reflect.Array {
newVal, err = collections.Append(existingAddend, newAddend)
if err != nil {
return "", err
}
} else {
newVal, err = math.DoArithmetic(existingAddend, newAddend, '+')
if err != nil {
return "", err
}
}
} else {
newVal = newAddend
}
c.mu.Lock()
c.values[key] = newVal
c.mu.Unlock()
return "", nil // have to return something to make it work with the Go templates
}
// Set stores a value with the given key in the Node context.
// This value can later be retrieved with Get.
func (c *Scratch) Set(key string, value any) string {
c.mu.Lock()
c.values[key] = value
c.mu.Unlock()
return ""
}
// Delete deletes the given key.
func (c *Scratch) Delete(key string) string {
c.mu.Lock()
delete(c.values, key)
c.mu.Unlock()
return ""
}
// Get returns a value previously set by Add or Set.
func (c *Scratch) Get(key string) any {
c.mu.RLock()
val := c.values[key]
c.mu.RUnlock()
return val
}
// Values returns the raw backing map. Note that you should just use
// this method on the locally scoped Scratch instances you obtain via newScratch, not
// .Page.Scratch etc., as that will lead to concurrency issues.
func (c *Scratch) Values() map[string]any {
c.mu.RLock()
defer c.mu.RUnlock()
return c.values
}
// SetInMap stores a value to a map with the given key in the Node context.
// This map can later be retrieved with GetSortedMapValues.
func (c *Scratch) SetInMap(key string, mapKey string, value any) string {
c.mu.Lock()
_, found := c.values[key]
if !found {
c.values[key] = make(map[string]any)
}
c.values[key].(map[string]any)[mapKey] = value
c.mu.Unlock()
return ""
}
// DeleteInMap deletes a value to a map with the given key in the Node context.
func (c *Scratch) DeleteInMap(key string, mapKey string) string {
c.mu.Lock()
_, found := c.values[key]
if found {
delete(c.values[key].(map[string]any), mapKey)
}
c.mu.Unlock()
return ""
}
// GetSortedMapValues returns a sorted map previously filled with SetInMap.
func (c *Scratch) GetSortedMapValues(key string) any {
c.mu.RLock()
if c.values[key] == nil {
c.mu.RUnlock()
return nil
}
unsortedMap := c.values[key].(map[string]any)
c.mu.RUnlock()
var keys []string
for mapKey := range unsortedMap {
keys = append(keys, mapKey)
}
sort.Strings(keys)
sortedArray := make([]any, len(unsortedMap))
for i, mapKey := range keys {
sortedArray[i] = unsortedMap[mapKey]
}
return sortedArray
}
// NewScratch returns a new instance of Scratch.
func NewScratch() *Scratch {
return &Scratch{values: make(map[string]any)}
}
// Copyright 2018 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 math
import (
"errors"
"reflect"
)
// DoArithmetic performs arithmetic operations (+,-,*,/) using reflection to
// determine the type of the two terms.
func DoArithmetic(a, b any, op rune) (any, error) {
av := reflect.ValueOf(a)
bv := reflect.ValueOf(b)
var ai, bi int64
var af, bf float64
var au, bu uint64
var isInt, isFloat, isUint bool
switch av.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
ai = av.Int()
switch bv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
isInt = true
bi = bv.Int()
case reflect.Float32, reflect.Float64:
isFloat = true
af = float64(ai) // may overflow
bf = bv.Float()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
bu = bv.Uint()
if ai >= 0 {
isUint = true
au = uint64(ai)
} else {
isInt = true
bi = int64(bu) // may overflow
}
default:
return nil, errors.New("can't apply the operator to the values")
}
case reflect.Float32, reflect.Float64:
isFloat = true
af = av.Float()
switch bv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
bf = float64(bv.Int()) // may overflow
case reflect.Float32, reflect.Float64:
bf = bv.Float()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
bf = float64(bv.Uint()) // may overflow
default:
return nil, errors.New("can't apply the operator to the values")
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
au = av.Uint()
switch bv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
bi = bv.Int()
if bi >= 0 {
isUint = true
bu = uint64(bi)
} else {
isInt = true
ai = int64(au) // may overflow
}
case reflect.Float32, reflect.Float64:
isFloat = true
af = float64(au) // may overflow
bf = bv.Float()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
isUint = true
bu = bv.Uint()
default:
return nil, errors.New("can't apply the operator to the values")
}
case reflect.String:
as := av.String()
if bv.Kind() == reflect.String && op == '+' {
bs := bv.String()
return as + bs, nil
}
return nil, errors.New("can't apply the operator to the values")
default:
return nil, errors.New("can't apply the operator to the values")
}
switch op {
case '+':
if isInt {
return ai + bi, nil
} else if isFloat {
return af + bf, nil
}
return au + bu, nil
case '-':
if isInt {
return ai - bi, nil
} else if isFloat {
return af - bf, nil
}
return au - bu, nil
case '*':
if isInt {
return ai * bi, nil
} else if isFloat {
return af * bf, nil
}
return au * bu, nil
case '/':
if isInt && bi != 0 {
return ai / bi, nil
} else if isFloat && bf != 0 {
return af / bf, nil
} else if isUint && bu != 0 {
return au / bu, nil
}
return nil, errors.New("can't divide the value by 0")
default:
return nil, errors.New("there is no such an operation")
}
}
// Copyright 2021 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 paths
import (
"errors"
"fmt"
"net/url"
"path"
"path/filepath"
"strings"
"unicode"
)
// FilePathSeparator as defined by os.Separator.
const (
FilePathSeparator = string(filepath.Separator)
slash = "/"
)
// filepathPathBridge is a bridge for common functionality in filepath vs path
type filepathPathBridge interface {
Base(in string) string
Clean(in string) string
Dir(in string) string
Ext(in string) string
Join(elem ...string) string
Separator() string
}
type filepathBridge struct{}
func (filepathBridge) Base(in string) string {
return filepath.Base(in)
}
func (filepathBridge) Clean(in string) string {
return filepath.Clean(in)
}
func (filepathBridge) Dir(in string) string {
return filepath.Dir(in)
}
func (filepathBridge) Ext(in string) string {
return filepath.Ext(in)
}
func (filepathBridge) Join(elem ...string) string {
return filepath.Join(elem...)
}
func (filepathBridge) Separator() string {
return FilePathSeparator
}
var fpb filepathBridge
// AbsPathify creates an absolute path if given a working dir and a relative path.
// If already absolute, the path is just cleaned.
func AbsPathify(workingDir, inPath string) string {
if filepath.IsAbs(inPath) {
return filepath.Clean(inPath)
}
return filepath.Join(workingDir, inPath)
}
// AddTrailingSlash adds a trailing Unix styled slash (/) if not already
// there.
func AddTrailingSlash(path string) string {
if !strings.HasSuffix(path, "/") {
path += "/"
}
return path
}
// AddLeadingSlash adds a leading Unix styled slash (/) if not already
// there.
func AddLeadingSlash(path string) string {
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return path
}
// AddTrailingAndLeadingSlash adds a leading and trailing Unix styled slash (/) if not already
// there.
func AddLeadingAndTrailingSlash(path string) string {
return AddTrailingSlash(AddLeadingSlash(path))
}
// MakeTitle converts the path given to a suitable title, trimming whitespace
// and replacing hyphens with whitespace.
func MakeTitle(inpath string) string {
return strings.Replace(strings.TrimSpace(inpath), "-", " ", -1)
}
// ReplaceExtension takes a path and an extension, strips the old extension
// and returns the path with the new extension.
func ReplaceExtension(path string, newExt string) string {
f, _ := fileAndExt(path, fpb)
return f + "." + newExt
}
func makePathRelative(inPath string, possibleDirectories ...string) (string, error) {
for _, currentPath := range possibleDirectories {
if strings.HasPrefix(inPath, currentPath) {
return strings.TrimPrefix(inPath, currentPath), nil
}
}
return inPath, errors.New("can't extract relative path, unknown prefix")
}
// ExtNoDelimiter takes a path and returns the extension, excluding the delimiter, i.e. "md".
func ExtNoDelimiter(in string) string {
return strings.TrimPrefix(Ext(in), ".")
}
// Ext takes a path and returns the extension, including the delimiter, i.e. ".md".
func Ext(in string) string {
_, ext := fileAndExt(in, fpb)
return ext
}
// PathAndExt is the same as FileAndExt, but it uses the path package.
func PathAndExt(in string) (string, string) {
return fileAndExt(in, pb)
}
// FileAndExt takes a path and returns the file and extension separated,
// the extension including the delimiter, i.e. ".md".
func FileAndExt(in string) (string, string) {
return fileAndExt(in, fpb)
}
// FileAndExtNoDelimiter takes a path and returns the file and extension separated,
// the extension excluding the delimiter, e.g "md".
func FileAndExtNoDelimiter(in string) (string, string) {
file, ext := fileAndExt(in, fpb)
return file, strings.TrimPrefix(ext, ".")
}
// Filename takes a file path, strips out the extension,
// and returns the name of the file.
func Filename(in string) (name string) {
name, _ = fileAndExt(in, fpb)
return
}
// FileAndExt returns the filename and any extension of a file path as
// two separate strings.
//
// If the path, in, contains a directory name ending in a slash,
// then both name and ext will be empty strings.
//
// If the path, in, is either the current directory, the parent
// directory or the root directory, or an empty string,
// then both name and ext will be empty strings.
//
// If the path, in, represents the path of a file without an extension,
// then name will be the name of the file and ext will be an empty string.
//
// If the path, in, represents a filename with an extension,
// then name will be the filename minus any extension - including the dot
// and ext will contain the extension - minus the dot.
func fileAndExt(in string, b filepathPathBridge) (name string, ext string) {
ext = b.Ext(in)
base := b.Base(in)
return extractFilename(in, ext, base, b.Separator()), ext
}
func extractFilename(in, ext, base, pathSeparator string) (name string) {
// No file name cases. These are defined as:
// 1. any "in" path that ends in a pathSeparator
// 2. any "base" consisting of just an pathSeparator
// 3. any "base" consisting of just an empty string
// 4. any "base" consisting of just the current directory i.e. "."
// 5. any "base" consisting of just the parent directory i.e. ".."
if (strings.LastIndex(in, pathSeparator) == len(in)-1) || base == "" || base == "." || base == ".." || base == pathSeparator {
name = "" // there is NO filename
} else if ext != "" { // there was an Extension
// return the filename minus the extension (and the ".")
name = base[:strings.LastIndex(base, ".")]
} else {
// no extension case so just return base, which will
// be the filename
name = base
}
return
}
// GetRelativePath returns the relative path of a given path.
func GetRelativePath(path, base string) (final string, err error) {
if filepath.IsAbs(path) && base == "" {
return "", errors.New("source: missing base directory")
}
name := filepath.Clean(path)
base = filepath.Clean(base)
name, err = filepath.Rel(base, name)
if err != nil {
return "", err
}
if strings.HasSuffix(filepath.FromSlash(path), FilePathSeparator) && !strings.HasSuffix(name, FilePathSeparator) {
name += FilePathSeparator
}
return name, nil
}
func prettifyPath(in string, b filepathPathBridge) string {
if filepath.Ext(in) == "" {
// /section/name/ -> /section/name/index.html
if len(in) < 2 {
return b.Separator()
}
return b.Join(in, "index.html")
}
name, ext := fileAndExt(in, b)
if name == "index" {
// /section/name/index.html -> /section/name/index.html
return b.Clean(in)
}
// /section/name.html -> /section/name/index.html
return b.Join(b.Dir(in), name, "index"+ext)
}
// CommonDirPath returns the common directory of the given paths.
func CommonDirPath(path1, path2 string) string {
if path1 == "" || path2 == "" {
return ""
}
hadLeadingSlash := strings.HasPrefix(path1, "/") || strings.HasPrefix(path2, "/")
path1 = TrimLeading(path1)
path2 = TrimLeading(path2)
p1 := strings.Split(path1, "/")
p2 := strings.Split(path2, "/")
var common []string
for i := 0; i < len(p1) && i < len(p2); i++ {
if p1[i] == p2[i] {
common = append(common, p1[i])
} else {
break
}
}
s := strings.Join(common, "/")
if hadLeadingSlash && s != "" {
s = "/" + s
}
return s
}
// Sanitize sanitizes string to be used in Hugo's file paths and URLs, allowing only
// a predefined set of special Unicode characters.
//
// Spaces will be replaced with a single hyphen.
//
// This function is the core function used to normalize paths in Hugo.
//
// Note that this is the first common step for URL/path sanitation,
// the final URL/path may end up looking differently if the user has stricter rules defined (e.g. removePathAccents=true).
func Sanitize(s string) string {
var willChange bool
for i, r := range s {
willChange = !isAllowedPathCharacter(s, i, r)
if willChange {
break
}
}
if !willChange {
// Prevent allocation when nothing changes.
return s
}
target := make([]rune, 0, len(s))
var (
prependHyphen bool
wasHyphen bool
)
for i, r := range s {
isAllowed := isAllowedPathCharacter(s, i, r)
if isAllowed {
// track explicit hyphen in input; no need to add a new hyphen if
// we just saw one.
wasHyphen = r == '-'
if prependHyphen {
// if currently have a hyphen, don't prepend an extra one
if !wasHyphen {
target = append(target, '-')
}
prependHyphen = false
}
target = append(target, r)
} else if len(target) > 0 && !wasHyphen && unicode.IsSpace(r) {
prependHyphen = true
}
}
return string(target)
}
func isAllowedPathCharacter(s string, i int, r rune) bool {
if r == ' ' {
return false
}
// Check for the most likely first (faster).
isAllowed := unicode.IsLetter(r) || unicode.IsDigit(r)
isAllowed = isAllowed || r == '.' || r == '/' || r == '\\' || r == '_' || r == '#' || r == '+' || r == '~' || r == '-' || r == '@'
isAllowed = isAllowed || unicode.IsMark(r)
isAllowed = isAllowed || (r == '%' && i+2 < len(s) && ishex(s[i+1]) && ishex(s[i+2]))
return isAllowed
}
// From https://golang.org/src/net/url/url.go
func ishex(c byte) bool {
switch {
case '0' <= c && c <= '9':
return true
case 'a' <= c && c <= 'f':
return true
case 'A' <= c && c <= 'F':
return true
}
return false
}
var slashFunc = func(r rune) bool {
return r == '/'
}
// Dir behaves like path.Dir without the path.Clean step.
//
// The returned path ends in a slash only if it is the root "/".
func Dir(s string) string {
dir, _ := path.Split(s)
if len(dir) > 1 && dir[len(dir)-1] == '/' {
return dir[:len(dir)-1]
}
return dir
}
// FieldsSlash cuts s into fields separated with '/'.
func FieldsSlash(s string) []string {
f := strings.FieldsFunc(s, slashFunc)
return f
}
// DirFile holds the result from path.Split.
type DirFile struct {
Dir string
File string
}
// Used in test.
func (df DirFile) String() string {
return fmt.Sprintf("%s|%s", df.Dir, df.File)
}
// PathEscape escapes unicode letters in pth.
// Use URLEscape to escape full URLs including scheme, query etc.
// This is slightly faster for the common case.
// Note, there is a url.PathEscape function, but that also
// escapes /.
func PathEscape(pth string) string {
u, err := url.Parse(pth)
if err != nil {
panic(err)
}
return u.EscapedPath()
}
// ToSlashTrimLeading is just a filepath.ToSlash with an added / prefix trimmer.
func ToSlashTrimLeading(s string) string {
return TrimLeading(filepath.ToSlash(s))
}
// TrimLeading trims the leading slash from the given string.
func TrimLeading(s string) string {
return strings.TrimPrefix(s, "/")
}
// ToSlashTrimTrailing is just a filepath.ToSlash with an added / suffix trimmer.
func ToSlashTrimTrailing(s string) string {
return TrimTrailing(filepath.ToSlash(s))
}
// TrimTrailing trims the trailing slash from the given string.
func TrimTrailing(s string) string {
return strings.TrimSuffix(s, "/")
}
// ToSlashTrim trims any leading and trailing slashes from the given string and converts it to a forward slash separated path.
func ToSlashTrim(s string) string {
return strings.Trim(filepath.ToSlash(s), "/")
}
// ToSlashPreserveLeading converts the path given to a forward slash separated path
// and preserves the leading slash if present trimming any trailing slash.
func ToSlashPreserveLeading(s string) string {
return "/" + strings.Trim(filepath.ToSlash(s), "/")
}
// IsSameFilePath checks if s1 and s2 are the same file path.
func IsSameFilePath(s1, s2 string) bool {
return path.Clean(ToSlashTrim(s1)) == path.Clean(ToSlashTrim(s2))
}
// Copyright 2024 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 paths
import (
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources/kinds"
)
const (
identifierBaseof = "baseof"
)
// PathParser parses a path into a Path.
type PathParser struct {
// Maps the language code to its index in the languages/sites slice.
LanguageIndex map[string]int
// Reports whether the given language is disabled.
IsLangDisabled func(string) bool
// IsOutputFormat reports whether the given name is a valid output format.
// The second argument is optional.
IsOutputFormat func(name, ext string) bool
// Reports whether the given ext is a content file.
IsContentExt func(string) bool
}
// NormalizePathString returns a normalized path string using the very basic Hugo rules.
func NormalizePathStringBasic(s string) string {
// All lower case.
s = strings.ToLower(s)
// Replace spaces with hyphens.
s = strings.ReplaceAll(s, " ", "-")
return s
}
// ParseIdentity parses component c with path s into a StringIdentity.
func (pp *PathParser) ParseIdentity(c, s string) identity.StringIdentity {
p := pp.parsePooled(c, s)
defer putPath(p)
return identity.StringIdentity(p.IdentifierBase())
}
// ParseBaseAndBaseNameNoIdentifier parses component c with path s into a base and a base name without any identifier.
func (pp *PathParser) ParseBaseAndBaseNameNoIdentifier(c, s string) (string, string) {
p := pp.parsePooled(c, s)
defer putPath(p)
return p.Base(), p.BaseNameNoIdentifier()
}
func (pp *PathParser) parsePooled(c, s string) *Path {
s = NormalizePathStringBasic(s)
p := getPath()
p.component = c
p, err := pp.doParse(c, s, p)
if err != nil {
panic(err)
}
return p
}
// Parse parses component c with path s into Path using Hugo's content path rules.
func (pp *PathParser) Parse(c, s string) *Path {
p, err := pp.parse(c, s)
if err != nil {
panic(err)
}
return p
}
func (pp *PathParser) newPath(component string) *Path {
p := &Path{}
p.reset()
p.component = component
return p
}
func (pp *PathParser) parse(component, s string) (*Path, error) {
ss := NormalizePathStringBasic(s)
p, err := pp.doParse(component, ss, pp.newPath(component))
if err != nil {
return nil, err
}
if s != ss {
var err error
// Preserve the original case for titles etc.
p.unnormalized, err = pp.doParse(component, s, pp.newPath(component))
if err != nil {
return nil, err
}
} else {
p.unnormalized = p
}
return p, nil
}
func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot, numDots int, isLast bool) {
if p.posContainerHigh != -1 {
return
}
mayHaveLang := numDots > 1 && p.posIdentifierLanguage == -1 && pp.LanguageIndex != nil
mayHaveLang = mayHaveLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
mayHaveOutputFormat := component == files.ComponentFolderLayouts
mayHaveKind := p.posIdentifierKind == -1 && mayHaveOutputFormat
var mayHaveLayout bool
if p.pathType == TypeShortcode {
mayHaveLayout = !isLast && component == files.ComponentFolderLayouts
} else {
mayHaveLayout = component == files.ComponentFolderLayouts
}
var found bool
var high int
if len(p.identifiersKnown) > 0 {
high = lastDot
} else {
high = len(p.s)
}
id := types.LowHigh[string]{Low: i + 1, High: high}
sid := p.s[id.Low:id.High]
if len(p.identifiersKnown) == 0 {
// The first is always the extension.
p.identifiersKnown = append(p.identifiersKnown, id)
found = true
// May also be the output format.
if mayHaveOutputFormat && pp.IsOutputFormat(sid, "") {
p.posIdentifierOutputFormat = 0
}
} else {
var langFound bool
if mayHaveLang {
var disabled bool
_, langFound = pp.LanguageIndex[sid]
if !langFound {
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(sid)
if disabled {
p.disabled = true
langFound = true
}
}
found = langFound
if langFound {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLanguage = len(p.identifiersKnown) - 1
}
}
if !found && mayHaveOutputFormat {
// At this point we may already have resolved an output format,
// but we need to keep looking for a more specific one, e.g. amp before html.
// Use both name and extension to prevent
// false positives on the form css.html.
if pp.IsOutputFormat(sid, p.Ext()) {
found = true
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierOutputFormat = len(p.identifiersKnown) - 1
}
}
if !found && mayHaveKind {
if kinds.GetKindMain(sid) != "" {
found = true
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierKind = len(p.identifiersKnown) - 1
}
}
if !found && sid == identifierBaseof {
found = true
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierBaseof = len(p.identifiersKnown) - 1
}
if !found && mayHaveLayout {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLayout = len(p.identifiersKnown) - 1
found = true
}
if !found {
p.identifiersUnknown = append(p.identifiersUnknown, id)
}
}
}
func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
if runtime.GOOS == "windows" {
s = path.Clean(filepath.ToSlash(s))
if s == "." {
s = ""
}
}
if s == "" {
s = "/"
}
// Leading slash, no trailing slash.
if !strings.HasPrefix(s, "/") {
s = "/" + s
}
if s != "/" && s[len(s)-1] == '/' {
s = s[:len(s)-1]
}
p.s = s
slashCount := 0
lastDot := 0
lastSlashIdx := strings.LastIndex(s, "/")
numDots := strings.Count(s[lastSlashIdx+1:], ".")
if strings.Contains(s, "/_shortcodes/") {
p.pathType = TypeShortcode
}
for i := len(s) - 1; i >= 0; i-- {
c := s[i]
switch c {
case '.':
pp.parseIdentifier(component, s, p, i, lastDot, numDots, false)
lastDot = i
case '/':
slashCount++
if p.posContainerHigh == -1 {
if lastDot > 0 {
pp.parseIdentifier(component, s, p, i, lastDot, numDots, true)
}
p.posContainerHigh = i + 1
} else if p.posContainerLow == -1 {
p.posContainerLow = i + 1
}
if i > 0 {
p.posSectionHigh = i
}
}
}
if len(p.identifiersKnown) > 0 {
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
isContent := isContentComponent && pp.IsContentExt(p.Ext())
id := p.identifiersKnown[len(p.identifiersKnown)-1]
if id.Low > p.posContainerHigh {
b := p.s[p.posContainerHigh : id.Low-1]
if isContent {
switch b {
case "index":
p.pathType = TypeLeaf
case "_index":
p.pathType = TypeBranch
default:
p.pathType = TypeContentSingle
}
if slashCount == 2 && p.IsLeafBundle() {
p.posSectionHigh = 0
}
} else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
p.pathType = TypeContentData
}
}
}
if p.pathType < TypeMarkup && component == files.ComponentFolderLayouts {
if p.posIdentifierBaseof != -1 {
p.pathType = TypeBaseof
} else {
pth := p.Path()
if strings.Contains(pth, "/_shortcodes/") {
p.pathType = TypeShortcode
} else if strings.Contains(pth, "/_markup/") {
p.pathType = TypeMarkup
} else if strings.HasPrefix(pth, "/_partials/") {
p.pathType = TypePartial
}
}
}
if p.pathType == TypeShortcode && p.posIdentifierLayout != -1 {
id := p.identifiersKnown[p.posIdentifierLayout]
if id.Low == p.posContainerHigh {
// First identifier is shortcode name.
p.posIdentifierLayout = -1
}
}
return p, nil
}
func ModifyPathBundleTypeResource(p *Path) {
if p.IsContent() {
p.pathType = TypeContentResource
} else {
p.pathType = TypeFile
}
}
//go:generate stringer -type Type
type Type int
const (
// A generic resource, e.g. a JSON file.
TypeFile Type = iota
// All below are content files.
// A resource of a content type with front matter.
TypeContentResource
// E.g. /blog/my-post.md
TypeContentSingle
// All below are bundled content files.
// Leaf bundles, e.g. /blog/my-post/index.md
TypeLeaf
// Branch bundles, e.g. /blog/_index.md
TypeBranch
// Content data file, _content.gotmpl.
TypeContentData
// Layout types.
TypeMarkup
TypeShortcode
TypePartial
TypeBaseof
)
type Path struct {
// Note: Any additions to this struct should also be added to the pathPool.
s string
posContainerLow int
posContainerHigh int
posSectionHigh int
component string
pathType Type
identifiersKnown []types.LowHigh[string]
identifiersUnknown []types.LowHigh[string]
posIdentifierLanguage int
posIdentifierOutputFormat int
posIdentifierKind int
posIdentifierLayout int
posIdentifierBaseof int
disabled bool
trimLeadingSlash bool
unnormalized *Path
}
var pathPool = &sync.Pool{
New: func() any {
p := &Path{}
p.reset()
return p
},
}
func getPath() *Path {
return pathPool.Get().(*Path)
}
func putPath(p *Path) {
p.reset()
pathPool.Put(p)
}
func (p *Path) reset() {
p.s = ""
p.posContainerLow = -1
p.posContainerHigh = -1
p.posSectionHigh = -1
p.component = ""
p.pathType = 0
p.identifiersKnown = p.identifiersKnown[:0]
p.posIdentifierLanguage = -1
p.posIdentifierOutputFormat = -1
p.posIdentifierKind = -1
p.posIdentifierLayout = -1
p.posIdentifierBaseof = -1
p.disabled = false
p.trimLeadingSlash = false
p.unnormalized = nil
}
// TrimLeadingSlash returns a copy of the Path with the leading slash removed.
func (p Path) TrimLeadingSlash() *Path {
p.trimLeadingSlash = true
return &p
}
func (p *Path) norm(s string) string {
if p.trimLeadingSlash {
s = strings.TrimPrefix(s, "/")
}
return s
}
// IdentifierBase satisfies identity.Identity.
func (p *Path) IdentifierBase() string {
if p.Component() == files.ComponentFolderLayouts {
return p.Path()
}
return p.Base()
}
// Component returns the component for this path (e.g. "content").
func (p *Path) Component() string {
return p.component
}
// Container returns the base name of the container directory for this path.
func (p *Path) Container() string {
if p.posContainerLow == -1 {
return ""
}
return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1])
}
func (p *Path) String() string {
if p == nil {
return "<nil>"
}
return p.Path()
}
// ContainerDir returns the container directory for this path.
// For content bundles this will be the parent directory.
func (p *Path) ContainerDir() string {
if p.posContainerLow == -1 || !p.IsBundle() {
return p.Dir()
}
return p.norm(p.s[:p.posContainerLow-1])
}
// Section returns the first path element (section).
func (p *Path) Section() string {
if p.posSectionHigh <= 0 {
return ""
}
return p.norm(p.s[1:p.posSectionHigh])
}
// IsContent returns true if the path is a content file (e.g. mypost.md).
// Note that this will also return true for content files in a bundle.
func (p *Path) IsContent() bool {
return p.Type() >= TypeContentResource && p.Type() <= TypeContentData
}
// isContentPage returns true if the path is a content file (e.g. mypost.md),
// but nof if inside a leaf bundle.
func (p *Path) isContentPage() bool {
return p.Type() >= TypeContentSingle && p.Type() <= TypeContentData
}
// Name returns the last element of path.
func (p *Path) Name() string {
if p.posContainerHigh > 0 {
return p.s[p.posContainerHigh:]
}
return p.s
}
// Name returns the last element of path without any extension.
func (p *Path) NameNoExt() string {
if i := p.identifierIndex(0); i != -1 {
return p.s[p.posContainerHigh : p.identifiersKnown[i].Low-1]
}
return p.s[p.posContainerHigh:]
}
// Name returns the last element of path without any language identifier.
func (p *Path) NameNoLang() string {
i := p.identifierIndex(p.posIdentifierLanguage)
if i == -1 {
return p.Name()
}
return p.s[p.posContainerHigh:p.identifiersKnown[i].Low-1] + p.s[p.identifiersKnown[i].High:]
}
// BaseNameNoIdentifier returns the logical base name for a resource without any identifier (e.g. no extension).
// For bundles this will be the containing directory's name, e.g. "blog".
func (p *Path) BaseNameNoIdentifier() string {
if p.IsBundle() {
return p.Container()
}
return p.NameNoIdentifier()
}
// NameNoIdentifier returns the last element of path without any identifier (e.g. no extension).
func (p *Path) NameNoIdentifier() string {
lowHigh := p.nameLowHigh()
return p.s[lowHigh.Low:lowHigh.High]
}
func (p *Path) nameLowHigh() types.LowHigh[string] {
if len(p.identifiersKnown) > 0 {
lastID := p.identifiersKnown[len(p.identifiersKnown)-1]
if p.posContainerHigh == lastID.Low {
// The last identifier is the name.
return lastID
}
return types.LowHigh[string]{
Low: p.posContainerHigh,
High: p.identifiersKnown[len(p.identifiersKnown)-1].Low - 1,
}
}
return types.LowHigh[string]{
Low: p.posContainerHigh,
High: len(p.s),
}
}
// Dir returns all but the last element of path, typically the path's directory.
func (p *Path) Dir() (d string) {
if p.posContainerHigh > 0 {
d = p.s[:p.posContainerHigh-1]
}
if d == "" {
d = "/"
}
d = p.norm(d)
return
}
// Path returns the full path.
func (p *Path) Path() (d string) {
return p.norm(p.s)
}
// PathNoLeadingSlash returns the full path without the leading slash.
func (p *Path) PathNoLeadingSlash() string {
return p.Path()[1:]
}
// Unnormalized returns the Path with the original case preserved.
func (p *Path) Unnormalized() *Path {
return p.unnormalized
}
// PathNoLang returns the Path but with any language identifier removed.
func (p *Path) PathNoLang() string {
return p.base(true, false)
}
// PathNoIdentifier returns the Path but with any identifier (ext, lang) removed.
func (p *Path) PathNoIdentifier() string {
return p.base(false, false)
}
// PathBeforeLangAndOutputFormatAndExt returns the path up to the first identifier that is not a language or output format.
func (p *Path) PathBeforeLangAndOutputFormatAndExt() string {
if len(p.identifiersKnown) == 0 {
return p.norm(p.s)
}
i := p.identifierIndex(0)
if j := p.posIdentifierOutputFormat; i == -1 || (j != -1 && j < i) {
i = j
}
if j := p.posIdentifierLanguage; i == -1 || (j != -1 && j < i) {
i = j
}
if i == -1 {
return p.norm(p.s)
}
id := p.identifiersKnown[i]
return p.norm(p.s[:id.Low-1])
}
// PathRel returns the path relative to the given owner.
func (p *Path) PathRel(owner *Path) string {
ob := owner.Base()
if !strings.HasSuffix(ob, "/") {
ob += "/"
}
return strings.TrimPrefix(p.Path(), ob)
}
// BaseRel returns the base path relative to the given owner.
func (p *Path) BaseRel(owner *Path) string {
ob := owner.Base()
if ob == "/" {
ob = ""
}
return p.Base()[len(ob)+1:]
}
// For content files, Base returns the path without any identifiers (extension, language code etc.).
// Any 'index' as the last path element is ignored.
//
// For other files (Resources), any extension is kept.
func (p *Path) Base() string {
return p.base(!p.isContentPage(), p.IsBundle())
}
// Used in template lookups.
// For pages with Type set, we treat that as the section.
func (p *Path) BaseReTyped(typ string) (d string) {
base := p.Base()
if typ == "" || p.Section() == typ {
return base
}
d = "/" + typ
if p.posSectionHigh != -1 {
d += base[p.posSectionHigh:]
}
d = p.norm(d)
return
}
// BaseNoLeadingSlash returns the base path without the leading slash.
func (p *Path) BaseNoLeadingSlash() string {
return p.Base()[1:]
}
func (p *Path) base(preserveExt, isBundle bool) string {
if len(p.identifiersKnown) == 0 {
return p.norm(p.s)
}
if preserveExt && len(p.identifiersKnown) == 1 {
// Preserve extension.
return p.norm(p.s)
}
var high int
if isBundle {
high = p.posContainerHigh - 1
} else {
high = p.nameLowHigh().High
}
if high == 0 {
high++
}
if !preserveExt {
return p.norm(p.s[:high])
}
// For txt files etc. we want to preserve the extension.
id := p.identifiersKnown[0]
return p.norm(p.s[:high] + p.s[id.Low-1:id.High])
}
func (p *Path) Ext() string {
return p.identifierAsString(0)
}
func (p *Path) OutputFormat() string {
return p.identifierAsString(p.posIdentifierOutputFormat)
}
func (p *Path) Kind() string {
return p.identifierAsString(p.posIdentifierKind)
}
func (p *Path) Layout() string {
return p.identifierAsString(p.posIdentifierLayout)
}
func (p *Path) Lang() string {
return p.identifierAsString(p.posIdentifierLanguage)
}
func (p *Path) Identifier(i int) string {
return p.identifierAsString(i)
}
func (p *Path) Disabled() bool {
return p.disabled
}
func (p *Path) Identifiers() []string {
ids := make([]string, len(p.identifiersKnown))
for i, id := range p.identifiersKnown {
ids[i] = p.s[id.Low:id.High]
}
return ids
}
func (p *Path) IdentifiersUnknown() []string {
ids := make([]string, len(p.identifiersUnknown))
for i, id := range p.identifiersUnknown {
ids[i] = p.s[id.Low:id.High]
}
return ids
}
func (p *Path) Type() Type {
return p.pathType
}
func (p *Path) IsBundle() bool {
return p.pathType >= TypeLeaf && p.pathType <= TypeContentData
}
func (p *Path) IsBranchBundle() bool {
return p.pathType == TypeBranch
}
func (p *Path) IsLeafBundle() bool {
return p.pathType == TypeLeaf
}
func (p *Path) IsContentData() bool {
return p.pathType == TypeContentData
}
func (p Path) ForType(t Type) *Path {
p.pathType = t
return &p
}
func (p *Path) identifierAsString(i int) string {
i = p.identifierIndex(i)
if i == -1 {
return ""
}
id := p.identifiersKnown[i]
return p.s[id.Low:id.High]
}
func (p *Path) identifierIndex(i int) int {
if i < 0 || i >= len(p.identifiersKnown) {
return -1
}
return i
}
// HasExt returns true if the Unix styled path has an extension.
func HasExt(p string) bool {
for i := len(p) - 1; i >= 0; i-- {
if p[i] == '.' {
return true
}
if p[i] == '/' {
return false
}
}
return false
}
// Code generated by "stringer -type Type"; DO NOT EDIT.
package paths
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[TypeFile-0]
_ = x[TypeContentResource-1]
_ = x[TypeContentSingle-2]
_ = x[TypeLeaf-3]
_ = x[TypeBranch-4]
_ = x[TypeContentData-5]
_ = x[TypeMarkup-6]
_ = x[TypeShortcode-7]
_ = x[TypePartial-8]
_ = x[TypeBaseof-9]
}
const _Type_name = "TypeFileTypeContentResourceTypeContentSingleTypeLeafTypeBranchTypeContentDataTypeMarkupTypeShortcodeTypePartialTypeBaseof"
var _Type_index = [...]uint8{0, 8, 27, 44, 52, 62, 77, 87, 100, 111, 121}
func (i Type) String() string {
if i < 0 || i >= Type(len(_Type_index)-1) {
return "Type(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Type_name[_Type_index[i]:_Type_index[i+1]]
}
// Copyright 2024 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 paths
import (
"fmt"
"net/url"
"path"
"path/filepath"
"runtime"
"strings"
)
type pathBridge struct{}
func (pathBridge) Base(in string) string {
return path.Base(in)
}
func (pathBridge) Clean(in string) string {
return path.Clean(in)
}
func (pathBridge) Dir(in string) string {
return path.Dir(in)
}
func (pathBridge) Ext(in string) string {
return path.Ext(in)
}
func (pathBridge) Join(elem ...string) string {
return path.Join(elem...)
}
func (pathBridge) Separator() string {
return "/"
}
var pb pathBridge
// MakePermalink combines base URL with content path to create full URL paths.
// Example
//
// base: http://spf13.com/
// path: post/how-i-blog
// result: http://spf13.com/post/how-i-blog
func MakePermalink(host, plink string) *url.URL {
base, err := url.Parse(host)
if err != nil {
panic(err)
}
p, err := url.Parse(plink)
if err != nil {
panic(err)
}
if p.Host != "" {
panic(fmt.Errorf("can't make permalink from absolute link %q", plink))
}
base.Path = path.Join(base.Path, p.Path)
base.Fragment = p.Fragment
base.RawQuery = p.RawQuery
// path.Join will strip off the last /, so put it back if it was there.
hadTrailingSlash := (plink == "" && strings.HasSuffix(host, "/")) || strings.HasSuffix(p.Path, "/")
if hadTrailingSlash && !strings.HasSuffix(base.Path, "/") {
base.Path = base.Path + "/"
}
return base
}
// AddContextRoot adds the context root to an URL if it's not already set.
// For relative URL entries on sites with a base url with a context root set (i.e. http://example.com/mysite),
// relative URLs must not include the context root if canonifyURLs is enabled. But if it's disabled, it must be set.
func AddContextRoot(baseURL, relativePath string) string {
url, err := url.Parse(baseURL)
if err != nil {
panic(err)
}
newPath := path.Join(url.Path, relativePath)
// path strips trailing slash, ignore root path.
if newPath != "/" && strings.HasSuffix(relativePath, "/") {
newPath += "/"
}
return newPath
}
// URLizeAn
// PrettifyURL takes a URL string and returns a semantic, clean URL.
func PrettifyURL(in string) string {
x := PrettifyURLPath(in)
if path.Base(x) == "index.html" {
return path.Dir(x)
}
if in == "" {
return "/"
}
return x
}
// PrettifyURLPath takes a URL path to a content and converts it
// to enable pretty URLs.
//
// /section/name.html becomes /section/name/index.html
// /section/name/ becomes /section/name/index.html
// /section/name/index.html becomes /section/name/index.html
func PrettifyURLPath(in string) string {
return prettifyPath(in, pb)
}
// Uglify does the opposite of PrettifyURLPath().
//
// /section/name/index.html becomes /section/name.html
// /section/name/ becomes /section/name.html
// /section/name.html becomes /section/name.html
func Uglify(in string) string {
if path.Ext(in) == "" {
if len(in) < 2 {
return "/"
}
// /section/name/ -> /section/name.html
return path.Clean(in) + ".html"
}
name, ext := fileAndExt(in, pb)
if name == "index" {
// /section/name/index.html -> /section/name.html
d := path.Dir(in)
if len(d) > 1 {
return d + ext
}
return in
}
// /.xml -> /index.xml
if name == "" {
return path.Dir(in) + "index" + ext
}
// /section/name.html -> /section/name.html
return path.Clean(in)
}
// URLEscape escapes unicode letters.
func URLEscape(uri string) string {
// escape unicode letters
u, err := url.Parse(uri)
if err != nil {
panic(err)
}
return u.String()
}
// TrimExt trims the extension from a path..
func TrimExt(in string) string {
return strings.TrimSuffix(in, path.Ext(in))
}
// From https://github.com/golang/go/blob/e0c76d95abfc1621259864adb3d101cf6f1f90fc/src/cmd/go/internal/web/url.go#L45
func UrlFromFilename(filename string) (*url.URL, error) {
if !filepath.IsAbs(filename) {
return nil, fmt.Errorf("filepath must be absolute")
}
// If filename has a Windows volume name, convert the volume to a host and prefix
// per https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/.
if vol := filepath.VolumeName(filename); vol != "" {
if strings.HasPrefix(vol, `\\`) {
filename = filepath.ToSlash(filename[2:])
i := strings.IndexByte(filename, '/')
if i < 0 {
// A degenerate case.
// \\host.example.com (without a share name)
// becomes
// file://host.example.com/
return &url.URL{
Scheme: "file",
Host: filename,
Path: "/",
}, nil
}
// \\host.example.com\Share\path\to\file
// becomes
// file://host.example.com/Share/path/to/file
return &url.URL{
Scheme: "file",
Host: filename[:i],
Path: filepath.ToSlash(filename[i:]),
}, nil
}
// C:\path\to\file
// becomes
// file:///C:/path/to/file
return &url.URL{
Scheme: "file",
Path: "/" + filepath.ToSlash(filename),
}, nil
}
// /path/to/file
// becomes
// file:///path/to/file
return &url.URL{
Scheme: "file",
Path: filepath.ToSlash(filename),
}, nil
}
// UrlStringToFilename converts the URL s to a filename.
// If ParseRequestURI fails, the input is just converted to OS specific slashes and returned.
func UrlStringToFilename(s string) (string, bool) {
u, err := url.ParseRequestURI(s)
if err != nil {
return filepath.FromSlash(s), false
}
p := u.Path
if p == "" {
p, _ = url.QueryUnescape(u.Opaque)
return filepath.FromSlash(p), false
}
if runtime.GOOS != "windows" {
return p, true
}
if len(p) == 0 || p[0] != '/' {
return filepath.FromSlash(p), false
}
p = filepath.FromSlash(p)
if len(u.Host) == 1 {
// file://c/Users/...
return strings.ToUpper(u.Host) + ":" + p, true
}
if u.Host != "" && u.Host != "localhost" {
if filepath.VolumeName(u.Host) != "" {
return "", false
}
return `\\` + u.Host + p, true
}
if vol := filepath.VolumeName(p[1:]); vol == "" || strings.HasPrefix(vol, `\\`) {
return "", false
}
return p[1:], true
}
// Copyright 2024 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 terminal contains helper for the terminal, such as coloring output.
package terminal
import (
"fmt"
"os"
"strings"
isatty "github.com/mattn/go-isatty"
)
const (
errorColor = "\033[1;31m%s\033[0m"
warningColor = "\033[0;33m%s\033[0m"
noticeColor = "\033[1;36m%s\033[0m"
)
// PrintANSIColors returns false if NO_COLOR env variable is set,
// else IsTerminal(f).
func PrintANSIColors(f *os.File) bool {
if os.Getenv("NO_COLOR") != "" {
return false
}
return IsTerminal(f)
}
// IsTerminal return true if the file descriptor is terminal and the TERM
// environment variable isn't a dumb one.
func IsTerminal(f *os.File) bool {
fd := f.Fd()
return os.Getenv("TERM") != "dumb" && (isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd))
}
// Notice colorizes the string in a noticeable color.
func Notice(s string) string {
return colorize(s, noticeColor)
}
// Error colorizes the string in a colour that grabs attention.
func Error(s string) string {
return colorize(s, errorColor)
}
// Warning colorizes the string in a colour that warns.
func Warning(s string) string {
return colorize(s, warningColor)
}
// colorize s in color.
func colorize(s, color string) string {
s = fmt.Sprintf(color, doublePercent(s))
return singlePercent(s)
}
func doublePercent(str string) string {
return strings.Replace(str, "%", "%%", -1)
}
func singlePercent(str string) string {
return strings.Replace(str, "%%", "%", -1)
}
// Copyright 2018 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 text
import (
"fmt"
"os"
"strings"
"github.com/gohugoio/hugo/common/terminal"
)
// Positioner represents a thing that knows its position in a text file or stream,
// typically an error.
type Positioner interface {
// Position returns the current position.
// Useful in error logging, e.g. {{ errorf "error in code block: %s" .Position }}.
Position() Position
}
// Position holds a source position in a text file or stream.
type Position struct {
Filename string // filename, if any
Offset int // byte offset, starting at 0. It's set to -1 if not provided.
LineNumber int // line number, starting at 1
ColumnNumber int // column number, starting at 1 (character count per line)
}
func (pos Position) String() string {
if pos.Filename == "" {
pos.Filename = "<stream>"
}
return positionStringFormatfunc(pos)
}
// IsValid returns true if line number is > 0.
func (pos Position) IsValid() bool {
return pos.LineNumber > 0
}
var positionStringFormatfunc func(p Position) string
func createPositionStringFormatter(formatStr string) func(p Position) string {
if formatStr == "" {
formatStr = "\":file::line::col\""
}
identifiers := []string{":file", ":line", ":col"}
var identifiersFound []string
for i := range formatStr {
for _, id := range identifiers {
if strings.HasPrefix(formatStr[i:], id) {
identifiersFound = append(identifiersFound, id)
}
}
}
replacer := strings.NewReplacer(":file", "%s", ":line", "%d", ":col", "%d")
format := replacer.Replace(formatStr)
f := func(pos Position) string {
args := make([]any, len(identifiersFound))
for i, id := range identifiersFound {
switch id {
case ":file":
args[i] = pos.Filename
case ":line":
args[i] = pos.LineNumber
case ":col":
args[i] = pos.ColumnNumber
}
}
msg := fmt.Sprintf(format, args...)
if terminal.PrintANSIColors(os.Stdout) {
return terminal.Notice(msg)
}
return msg
}
return f
}
func init() {
positionStringFormatfunc = createPositionStringFormatter(os.Getenv("HUGO_FILE_LOG_FORMAT"))
}
// Copyright 2019 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 text
import (
"strings"
"sync"
"unicode"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
)
var accentTransformerPool = &sync.Pool{
New: func() any {
return transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
},
}
// RemoveAccents removes all accents from b.
func RemoveAccents(b []byte) []byte {
t := accentTransformerPool.Get().(transform.Transformer)
b, _, _ = transform.Bytes(t, b)
t.Reset()
accentTransformerPool.Put(t)
return b
}
// RemoveAccentsString removes all accents from s.
func RemoveAccentsString(s string) string {
t := accentTransformerPool.Get().(transform.Transformer)
s, _, _ = transform.String(t, s)
t.Reset()
accentTransformerPool.Put(t)
return s
}
// Chomp removes trailing newline characters from s.
func Chomp(s string) string {
return strings.TrimRightFunc(s, func(r rune) bool {
return r == '\n' || r == '\r'
})
}
// Puts adds a trailing \n none found.
func Puts(s string) string {
if s == "" || s[len(s)-1] == '\n' {
return s
}
return s + "\n"
}
// VisitLinesAfter calls the given function for each line, including newlines, in the given string.
func VisitLinesAfter(s string, fn func(line string)) {
high := strings.IndexRune(s, '\n')
for high != -1 {
fn(s[:high+1])
s = s[high+1:]
high = strings.IndexRune(s, '\n')
}
if s != "" {
fn(s)
}
}
// Copyright 2024 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 types
import "sync"
type Closer interface {
Close() error
}
// CloserFunc is a convenience type to create a Closer from a function.
type CloserFunc func() error
func (f CloserFunc) Close() error {
return f()
}
type CloseAdder interface {
Add(Closer)
}
type Closers struct {
mu sync.Mutex
cs []Closer
}
func (cs *Closers) Add(c Closer) {
cs.mu.Lock()
defer cs.mu.Unlock()
cs.cs = append(cs.cs, c)
}
func (cs *Closers) Close() error {
cs.mu.Lock()
defer cs.mu.Unlock()
for _, c := range cs.cs {
c.Close()
}
cs.cs = cs.cs[:0]
return nil
}
// Copyright 2019 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 types
import (
"encoding/json"
"fmt"
"html/template"
"reflect"
"time"
"github.com/spf13/cast"
)
// ToDuration converts v to time.Duration.
// See ToDurationE if you need to handle errors.
func ToDuration(v any) time.Duration {
d, _ := ToDurationE(v)
return d
}
// ToDurationE converts v to time.Duration.
func ToDurationE(v any) (time.Duration, error) {
if n := cast.ToInt(v); n > 0 {
return time.Duration(n) * time.Millisecond, nil
}
d, err := time.ParseDuration(cast.ToString(v))
if err != nil {
return 0, fmt.Errorf("cannot convert %v to time.Duration", v)
}
return d, nil
}
// ToStringSlicePreserveString is the same as ToStringSlicePreserveStringE,
// but it never fails.
func ToStringSlicePreserveString(v any) []string {
vv, _ := ToStringSlicePreserveStringE(v)
return vv
}
// ToStringSlicePreserveStringE converts v to a string slice.
// If v is a string, it will be wrapped in a string slice.
func ToStringSlicePreserveStringE(v any) ([]string, error) {
if v == nil {
return nil, nil
}
if sds, ok := v.(string); ok {
return []string{sds}, nil
}
result, err := cast.ToStringSliceE(v)
if err == nil {
return result, nil
}
// Probably []int or similar. Fall back to reflect.
vv := reflect.ValueOf(v)
switch vv.Kind() {
case reflect.Slice, reflect.Array:
result = make([]string, vv.Len())
for i := range vv.Len() {
s, err := cast.ToStringE(vv.Index(i).Interface())
if err != nil {
return nil, err
}
result[i] = s
}
return result, nil
default:
return nil, fmt.Errorf("failed to convert %T to a string slice", v)
}
}
// TypeToString converts v to a string if it's a valid string type.
// Note that this will not try to convert numeric values etc.,
// use ToString for that.
func TypeToString(v any) (string, bool) {
switch s := v.(type) {
case string:
return s, true
case template.HTML:
return string(s), true
case template.CSS:
return string(s), true
case template.HTMLAttr:
return string(s), true
case template.JS:
return string(s), true
case template.JSStr:
return string(s), true
case template.URL:
return string(s), true
case template.Srcset:
return string(s), true
}
return "", false
}
// ToString converts v to a string.
func ToString(v any) string {
s, _ := ToStringE(v)
return s
}
// ToStringE converts v to a string.
func ToStringE(v any) (string, error) {
if s, ok := TypeToString(v); ok {
return s, nil
}
switch s := v.(type) {
case json.RawMessage:
return string(s), nil
default:
return cast.ToStringE(v)
}
}
// Copyright 2017-present 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 types contains types shared between packages in Hugo.
package types
import (
"slices"
"sync"
)
// EvictingQueue is a queue which automatically evicts elements from the head of
// the queue when attempting to add new elements onto the queue and it is full.
// This queue orders elements LIFO (last-in-first-out). It throws away duplicates.
type EvictingQueue[T comparable] struct {
size int
vals []T
set map[T]bool
mu sync.Mutex
zero T
}
// NewEvictingQueue creates a new queue with the given size.
func NewEvictingQueue[T comparable](size int) *EvictingQueue[T] {
return &EvictingQueue[T]{size: size, set: make(map[T]bool)}
}
// Add adds a new string to the tail of the queue if it's not already there.
func (q *EvictingQueue[T]) Add(v T) *EvictingQueue[T] {
q.mu.Lock()
if q.set[v] {
q.mu.Unlock()
return q
}
if len(q.set) == q.size {
// Full
delete(q.set, q.vals[0])
q.vals = slices.Delete(q.vals, 0, 1)
}
q.set[v] = true
q.vals = append(q.vals, v)
q.mu.Unlock()
return q
}
func (q *EvictingQueue[T]) Len() int {
if q == nil {
return 0
}
q.mu.Lock()
defer q.mu.Unlock()
return len(q.vals)
}
// Contains returns whether the queue contains v.
func (q *EvictingQueue[T]) Contains(v T) bool {
if q == nil {
return false
}
q.mu.Lock()
defer q.mu.Unlock()
return q.set[v]
}
// Peek looks at the last element added to the queue.
func (q *EvictingQueue[T]) Peek() T {
q.mu.Lock()
l := len(q.vals)
if l == 0 {
q.mu.Unlock()
return q.zero
}
elem := q.vals[l-1]
q.mu.Unlock()
return elem
}
// PeekAll looks at all the elements in the queue, with the newest first.
func (q *EvictingQueue[T]) PeekAll() []T {
if q == nil {
return nil
}
q.mu.Lock()
vals := make([]T, len(q.vals))
copy(vals, q.vals)
q.mu.Unlock()
for i, j := 0, len(vals)-1; i < j; i, j = i+1, j-1 {
vals[i], vals[j] = vals[j], vals[i]
}
return vals
}
// PeekAllSet returns PeekAll as a set.
func (q *EvictingQueue[T]) PeekAllSet() map[T]bool {
all := q.PeekAll()
set := make(map[T]bool)
for _, v := range all {
set[v] = true
}
return set
}
// Copyright 2019 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 types contains types shared between packages in Hugo.
package types
import (
"fmt"
"reflect"
"sync/atomic"
"github.com/spf13/cast"
)
// RLocker represents the read locks in sync.RWMutex.
type RLocker interface {
RLock()
RUnlock()
}
type Locker interface {
Lock()
Unlock()
}
type RWLocker interface {
RLocker
Locker
}
// KeyValue is a interface{} tuple.
type KeyValue struct {
Key any
Value any
}
// KeyValueStr is a string tuple.
type KeyValueStr struct {
Key string
Value string
}
// KeyValues holds an key and a slice of values.
type KeyValues struct {
Key any
Values []any
}
// KeyString returns the key as a string, an empty string if conversion fails.
func (k KeyValues) KeyString() string {
return cast.ToString(k.Key)
}
func (k KeyValues) String() string {
return fmt.Sprintf("%v: %v", k.Key, k.Values)
}
// NewKeyValuesStrings takes a given key and slice of values and returns a new
// KeyValues struct.
func NewKeyValuesStrings(key string, values ...string) KeyValues {
iv := make([]any, len(values))
for i := range values {
iv[i] = values[i]
}
return KeyValues{Key: key, Values: iv}
}
// Zeroer, as implemented by time.Time, will be used by the truth template
// funcs in Hugo (if, with, not, and, or).
type Zeroer interface {
IsZero() bool
}
// IsNil reports whether v is nil.
func IsNil(v any) bool {
if v == nil {
return true
}
value := reflect.ValueOf(v)
switch value.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return value.IsNil()
}
return false
}
// DevMarker is a marker interface for types that should only be used during
// development.
type DevMarker interface {
DevOnly()
}
// Unwrapper is implemented by types that can unwrap themselves.
type Unwrapper interface {
// Unwrapv is for internal use only.
// It got its slightly odd name to prevent collisions with user types.
Unwrapv() any
}
// Unwrap returns the underlying value of v if it implements Unwrapper, otherwise v is returned.
func Unwrapv(v any) any {
if u, ok := v.(Unwrapper); ok {
return u.Unwrapv()
}
return v
}
// LowHigh represents a byte or slice boundary.
type LowHigh[S ~[]byte | string] struct {
Low int
High int
}
func (l LowHigh[S]) IsZero() bool {
return l.Low < 0 || (l.Low == 0 && l.High == 0)
}
func (l LowHigh[S]) Value(source S) S {
return source[l.Low:l.High]
}
// This is only used for debugging purposes.
var InvocationCounter atomic.Int64
// NewTrue returns a pointer to b.
func NewBool(b bool) *bool {
return &b
}
// PrintableValueProvider is implemented by types that can provide a printable value.
type PrintableValueProvider interface {
PrintableValue() any
}
// Copyright 2019 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 compare
// Eqer can be used to determine if this value is equal to the other.
// The semantics of equals is that the two value are interchangeable
// in the Hugo templates.
type Eqer interface {
// Eq returns whether this value is equal to the other.
// This is for internal use.
Eq(other any) bool
}
// ProbablyEqer is an equal check that may return false positives, but never
// a false negative.
type ProbablyEqer interface {
// For internal use.
ProbablyEq(other any) bool
}
// Comparer can be used to compare two values.
// This will be used when using the le, ge etc. operators in the templates.
// Compare returns -1 if the given version is less than, 0 if equal and 1 if greater than
// the running version.
type Comparer interface {
Compare(other any) int
}
// Eq returns whether v1 is equal to v2.
// It will use the Eqer interface if implemented, which
// defines equals when two value are interchangeable
// in the Hugo templates.
func Eq(v1, v2 any) bool {
if v1 == nil || v2 == nil {
return v1 == v2
}
if eqer, ok := v1.(Eqer); ok {
return eqer.Eq(v2)
}
return v1 == v2
}
// ProbablyEq returns whether v1 is probably equal to v2.
func ProbablyEq(v1, v2 any) bool {
if Eq(v1, v2) {
return true
}
if peqer, ok := v1.(ProbablyEqer); ok {
return peqer.ProbablyEq(v2)
}
return false
}
// Copyright 2019 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 compare
import (
"strings"
"unicode"
"unicode/utf8"
)
// Strings returns an integer comparing two strings lexicographically.
func Strings(s, t string) int {
c := compareFold(s, t)
if c == 0 {
// "B" and "b" would be the same so we need a tiebreaker.
return strings.Compare(s, t)
}
return c
}
// This function is derived from strings.EqualFold in Go's stdlib.
// https://github.com/golang/go/blob/ad4a58e31501bce5de2aad90a620eaecdc1eecb8/src/strings/strings.go#L893
func compareFold(s, t string) int {
for s != "" && t != "" {
var sr, tr rune
if s[0] < utf8.RuneSelf {
sr, s = rune(s[0]), s[1:]
} else {
r, size := utf8.DecodeRuneInString(s)
sr, s = r, s[size:]
}
if t[0] < utf8.RuneSelf {
tr, t = rune(t[0]), t[1:]
} else {
r, size := utf8.DecodeRuneInString(t)
tr, t = r, t[size:]
}
if tr == sr {
continue
}
c := 1
if tr < sr {
tr, sr = sr, tr
c = -c
}
// ASCII only.
if tr < utf8.RuneSelf {
if sr >= 'A' && sr <= 'Z' {
if tr <= 'Z' {
// Same case.
return -c
}
diff := tr - (sr + 'a' - 'A')
if diff == 0 {
continue
}
if diff < 0 {
return c
}
if diff > 0 {
return -c
}
}
}
// Unicode.
r := unicode.SimpleFold(sr)
for r != sr && r < tr {
r = unicode.SimpleFold(r)
}
if r == tr {
continue
}
return -c
}
if s == "" && t == "" {
return 0
}
if s == "" {
return -1
}
return 1
}
// LessStrings returns whether s is less than t lexicographically.
func LessStrings(s, t string) bool {
return Strings(s, t) < 0
}
// Copyright 2019 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 files
import (
"os"
"path/filepath"
"sort"
"strings"
)
const (
// The NPM package.json "template" file.
FilenamePackageHugoJSON = "package.hugo.json"
// The NPM package file.
FilenamePackageJSON = "package.json"
FilenameHugoStatsJSON = "hugo_stats.json"
)
func IsGoTmplExt(ext string) bool {
return ext == "gotmpl"
}
// Supported data file extensions for _content.* files.
func IsContentDataExt(ext string) bool {
return IsGoTmplExt(ext)
}
const (
ComponentFolderArchetypes = "archetypes"
ComponentFolderStatic = "static"
ComponentFolderLayouts = "layouts"
ComponentFolderContent = "content"
ComponentFolderData = "data"
ComponentFolderAssets = "assets"
ComponentFolderI18n = "i18n"
FolderResources = "resources"
FolderJSConfig = "_jsconfig" // Mounted below /assets with postcss.config.js etc.
NameContentData = "_content"
)
var (
JsConfigFolderMountPrefix = filepath.Join(ComponentFolderAssets, FolderJSConfig)
ComponentFolders = []string{
ComponentFolderArchetypes,
ComponentFolderStatic,
ComponentFolderLayouts,
ComponentFolderContent,
ComponentFolderData,
ComponentFolderAssets,
ComponentFolderI18n,
}
componentFoldersSet = make(map[string]bool)
)
func init() {
sort.Strings(ComponentFolders)
for _, f := range ComponentFolders {
componentFoldersSet[f] = true
}
}
// ResolveComponentFolder returns "content" from "content/blog/foo.md" etc.
func ResolveComponentFolder(filename string) string {
filename = strings.TrimPrefix(filename, string(os.PathSeparator))
for _, cf := range ComponentFolders {
if strings.HasPrefix(filename, cf) {
return cf
}
}
return ""
}
func IsComponentFolder(name string) bool {
return componentFoldersSet[name]
}
// Copyright 2021 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 glob
import (
"path"
"path/filepath"
"strings"
"github.com/gobwas/glob"
)
type FilenameFilter struct {
shouldInclude func(filename string) bool
inclusions []glob.Glob
dirInclusions []glob.Glob
exclusions []glob.Glob
isWindows bool
nested []*FilenameFilter
}
func normalizeFilenameGlobPattern(s string) string {
// Use Unix separators even on Windows.
s = filepath.ToSlash(s)
if !strings.HasPrefix(s, "/") {
s = "/" + s
}
return s
}
// NewFilenameFilter creates a new Glob where the Match method will
// return true if the file should be included.
// Note that the inclusions will be checked first.
func NewFilenameFilter(inclusions, exclusions []string) (*FilenameFilter, error) {
if inclusions == nil && exclusions == nil {
return nil, nil
}
filter := &FilenameFilter{isWindows: isWindows}
for _, include := range inclusions {
include = normalizeFilenameGlobPattern(include)
g, err := GetGlob(include)
if err != nil {
return nil, err
}
filter.inclusions = append(filter.inclusions, g)
// For mounts that do directory walking (e.g. content) we
// must make sure that all directories up to this inclusion also
// gets included.
dir := path.Dir(include)
parts := strings.Split(dir, "/")
for i := range parts {
pattern := "/" + filepath.Join(parts[:i+1]...)
g, err := GetGlob(pattern)
if err != nil {
return nil, err
}
filter.dirInclusions = append(filter.dirInclusions, g)
}
}
for _, exclude := range exclusions {
exclude = normalizeFilenameGlobPattern(exclude)
g, err := GetGlob(exclude)
if err != nil {
return nil, err
}
filter.exclusions = append(filter.exclusions, g)
}
return filter, nil
}
// MustNewFilenameFilter invokes NewFilenameFilter and panics on error.
func MustNewFilenameFilter(inclusions, exclusions []string) *FilenameFilter {
filter, err := NewFilenameFilter(inclusions, exclusions)
if err != nil {
panic(err)
}
return filter
}
// NewFilenameFilterForInclusionFunc create a new filter using the provided inclusion func.
func NewFilenameFilterForInclusionFunc(shouldInclude func(filename string) bool) *FilenameFilter {
return &FilenameFilter{shouldInclude: shouldInclude, isWindows: isWindows}
}
// Match returns whether filename should be included.
func (f *FilenameFilter) Match(filename string, isDir bool) bool {
if f == nil {
return true
}
if !f.doMatch(filename, isDir) {
return false
}
for _, nested := range f.nested {
if !nested.Match(filename, isDir) {
return false
}
}
return true
}
// Append appends a filter to the chain. The receiver will be copied if needed.
func (f *FilenameFilter) Append(other *FilenameFilter) *FilenameFilter {
if f == nil {
return other
}
clone := *f
nested := make([]*FilenameFilter, len(clone.nested)+1)
copy(nested, clone.nested)
nested[len(nested)-1] = other
clone.nested = nested
return &clone
}
func (f *FilenameFilter) doMatch(filename string, isDir bool) bool {
if f == nil {
return true
}
if !strings.HasPrefix(filename, filepathSeparator) {
filename = filepathSeparator + filename
}
if f.shouldInclude != nil {
if f.shouldInclude(filename) {
return true
}
if f.isWindows {
// The Glob matchers below handles this by themselves,
// for the shouldInclude we need to take some extra steps
// to make this robust.
winFilename := filepath.FromSlash(filename)
if filename != winFilename {
if f.shouldInclude(winFilename) {
return true
}
}
}
}
for _, inclusion := range f.inclusions {
if inclusion.Match(filename) {
return true
}
}
if isDir && f.inclusions != nil {
for _, inclusion := range f.dirInclusions {
if inclusion.Match(filename) {
return true
}
}
}
for _, exclusion := range f.exclusions {
if exclusion.Match(filename) {
return false
}
}
return f.inclusions == nil && f.shouldInclude == nil
}
// Copyright 2021 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 glob
import (
"os"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/gobwas/glob"
"github.com/gobwas/glob/syntax"
)
const filepathSeparator = string(os.PathSeparator)
var (
isWindows = runtime.GOOS == "windows"
defaultGlobCache = &globCache{
isWindows: isWindows,
cache: make(map[string]globErr),
}
)
type globErr struct {
glob glob.Glob
err error
}
type globCache struct {
// Config
isWindows bool
// Cache
sync.RWMutex
cache map[string]globErr
}
func (gc *globCache) GetGlob(pattern string) (glob.Glob, error) {
var eg globErr
gc.RLock()
var found bool
eg, found = gc.cache[pattern]
gc.RUnlock()
if found {
return eg.glob, eg.err
}
var g glob.Glob
var err error
pattern = filepath.ToSlash(pattern)
g, err = glob.Compile(strings.ToLower(pattern), '/')
eg = globErr{
globDecorator{
g: g,
isWindows: gc.isWindows,
},
err,
}
gc.Lock()
gc.cache[pattern] = eg
gc.Unlock()
return eg.glob, eg.err
}
// Or creates a new Glob from the given globs.
func Or(globs ...glob.Glob) glob.Glob {
return globSlice{globs: globs}
}
// MatchesFunc is a convenience type to create a glob.Glob from a function.
type MatchesFunc func(s string) bool
func (m MatchesFunc) Match(s string) bool {
return m(s)
}
type globSlice struct {
globs []glob.Glob
}
func (g globSlice) Match(s string) bool {
for _, g := range g.globs {
if g.Match(s) {
return true
}
}
return false
}
type globDecorator struct {
// On Windows we may get filenames with Windows slashes to match,
// which we need to normalize.
isWindows bool
g glob.Glob
}
func (g globDecorator) Match(s string) bool {
if g.isWindows {
s = filepath.ToSlash(s)
}
s = strings.ToLower(s)
return g.g.Match(s)
}
func GetGlob(pattern string) (glob.Glob, error) {
return defaultGlobCache.GetGlob(pattern)
}
func NormalizePath(p string) string {
return strings.ToLower(NormalizePathNoLower(p))
}
func NormalizePathNoLower(p string) string {
return strings.Trim(path.Clean(filepath.ToSlash(p)), "/.")
}
// ResolveRootDir takes a normalized path on the form "assets/**.json" and
// determines any root dir, i.e. any start path without any wildcards.
func ResolveRootDir(p string) string {
parts := strings.Split(path.Dir(p), "/")
var roots []string
for _, part := range parts {
if HasGlobChar(part) {
break
}
roots = append(roots, part)
}
if len(roots) == 0 {
return ""
}
return strings.Join(roots, "/")
}
// FilterGlobParts removes any string with glob wildcard.
func FilterGlobParts(a []string) []string {
b := a[:0]
for _, x := range a {
if !HasGlobChar(x) {
b = append(b, x)
}
}
return b
}
// HasGlobChar returns whether s contains any glob wildcards.
func HasGlobChar(s string) bool {
for i := range len(s) {
if syntax.Special(s[i]) {
return true
}
}
return false
}
// Copyright 2024 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 identity
import (
"fmt"
"sync"
"github.com/gohugoio/hugo/compare"
)
// NewFinder creates a new Finder.
// This is a thread safe implementation with a cache.
func NewFinder(cfg FinderConfig) *Finder {
return &Finder{cfg: cfg, answers: make(map[ManagerIdentity]FinderResult), seenFindOnce: make(map[Identity]bool)}
}
var searchIDPool = sync.Pool{
New: func() any {
return &searchID{seen: make(map[Manager]bool)}
},
}
func getSearchID() *searchID {
return searchIDPool.Get().(*searchID)
}
func putSearchID(sid *searchID) {
sid.id = nil
sid.isDp = false
sid.isPeq = false
sid.hasEqer = false
sid.maxDepth = 0
sid.dp = nil
sid.peq = nil
sid.eqer = nil
clear(sid.seen)
searchIDPool.Put(sid)
}
// GetSearchID returns a searchID from the pool.
// Finder finds identities inside another.
type Finder struct {
cfg FinderConfig
answers map[ManagerIdentity]FinderResult
muAnswers sync.RWMutex
seenFindOnce map[Identity]bool
muSeenFindOnce sync.RWMutex
}
type FinderResult int
const (
FinderNotFound FinderResult = iota
FinderFoundOneOfManyRepetition
FinderFoundOneOfMany
FinderFound
)
// Contains returns whether in contains id.
func (f *Finder) Contains(id, in Identity, maxDepth int) FinderResult {
if id == Anonymous || in == Anonymous {
return FinderNotFound
}
if id == GenghisKhan && in == GenghisKhan {
return FinderNotFound
}
if id == GenghisKhan {
return FinderFound
}
if id == in {
return FinderFound
}
if id == nil || in == nil {
return FinderNotFound
}
var (
isDp bool
isPeq bool
dp IsProbablyDependentProvider
peq compare.ProbablyEqer
)
if !f.cfg.Exact {
dp, isDp = id.(IsProbablyDependentProvider)
peq, isPeq = id.(compare.ProbablyEqer)
}
eqer, hasEqer := id.(compare.Eqer)
sid := getSearchID()
sid.id = id
sid.isDp = isDp
sid.isPeq = isPeq
sid.hasEqer = hasEqer
sid.dp = dp
sid.peq = peq
sid.eqer = eqer
sid.maxDepth = maxDepth
defer putSearchID(sid)
r := FinderNotFound
if i := f.checkOne(sid, in, 0); i > r {
r = i
}
if r == FinderFound {
return r
}
m := GetDependencyManager(in)
if m != nil {
if i := f.checkManager(sid, m, 0); i > r {
r = i
}
}
return r
}
func (f *Finder) checkMaxDepth(sid *searchID, level int) FinderResult {
if sid.maxDepth >= 0 && level > sid.maxDepth {
return FinderNotFound
}
if level > 100 {
// This should never happen, but some false positives are probably better than a panic.
if !f.cfg.Exact {
return FinderFound
}
panic("too many levels")
}
return -1
}
func (f *Finder) checkManager(sid *searchID, m Manager, level int) FinderResult {
if r := f.checkMaxDepth(sid, level); r >= 0 {
return r
}
if m == nil {
return FinderNotFound
}
if sid.seen[m] {
return FinderNotFound
}
sid.seen[m] = true
f.muAnswers.RLock()
r, ok := f.answers[ManagerIdentity{Manager: m, Identity: sid.id}]
f.muAnswers.RUnlock()
if ok {
return r
}
r = f.search(sid, m, level)
if r == FinderFoundOneOfMany {
// Don't cache this one.
return r
}
f.muAnswers.Lock()
f.answers[ManagerIdentity{Manager: m, Identity: sid.id}] = r
f.muAnswers.Unlock()
return r
}
func (f *Finder) checkOne(sid *searchID, v Identity, depth int) (r FinderResult) {
if ff, ok := v.(FindFirstManagerIdentityProvider); ok {
f.muSeenFindOnce.RLock()
mi := ff.FindFirstManagerIdentity()
seen := f.seenFindOnce[mi.Identity]
f.muSeenFindOnce.RUnlock()
if seen {
return FinderFoundOneOfManyRepetition
}
r = f.doCheckOne(sid, mi.Identity, depth)
if r == 0 {
r = f.checkManager(sid, mi.Manager, depth)
}
if r > FinderFoundOneOfManyRepetition {
f.muSeenFindOnce.Lock()
// Double check.
if f.seenFindOnce[mi.Identity] {
f.muSeenFindOnce.Unlock()
return FinderFoundOneOfManyRepetition
}
f.seenFindOnce[mi.Identity] = true
f.muSeenFindOnce.Unlock()
r = FinderFoundOneOfMany
}
return r
} else {
return f.doCheckOne(sid, v, depth)
}
}
func (f *Finder) doCheckOne(sid *searchID, v Identity, depth int) FinderResult {
id2 := Unwrap(v)
if id2 == Anonymous {
return FinderNotFound
}
id := sid.id
if sid.hasEqer {
if sid.eqer.Eq(id2) {
return FinderFound
}
} else if id == id2 {
return FinderFound
}
if f.cfg.Exact {
return FinderNotFound
}
if id2 == nil {
return FinderNotFound
}
if id2 == GenghisKhan {
return FinderFound
}
if id.IdentifierBase() == id2.IdentifierBase() {
return FinderFound
}
if sid.isDp && sid.dp.IsProbablyDependent(id2) {
return FinderFound
}
if sid.isPeq && sid.peq.ProbablyEq(id2) {
return FinderFound
}
if pdep, ok := id2.(IsProbablyDependencyProvider); ok && pdep.IsProbablyDependency(id) {
return FinderFound
}
if peq, ok := id2.(compare.ProbablyEqer); ok && peq.ProbablyEq(id) {
return FinderFound
}
return FinderNotFound
}
// search searches for id in ids.
func (f *Finder) search(sid *searchID, m Manager, depth int) FinderResult {
id := sid.id
if id == Anonymous {
return FinderNotFound
}
if !f.cfg.Exact && id == GenghisKhan {
return FinderNotFound
}
var r FinderResult
m.forEeachIdentity(
func(v Identity) bool {
i := f.checkOne(sid, v, depth)
if i > r {
r = i
}
if r == FinderFound {
return true
}
m := GetDependencyManager(v)
if i := f.checkManager(sid, m, depth+1); i > r {
r = i
}
if r == FinderFound {
return true
}
return false
},
)
return r
}
// FinderConfig provides configuration for the Finder.
// Note that we by default will use a strategy where probable matches are
// good enough. The primary use case for this is to identity the change set
// for a given changed identity (e.g. a template), and we don't want to
// have any false negatives there, but some false positives are OK. Also, speed is important.
type FinderConfig struct {
// Match exact matches only.
Exact bool
}
// ManagerIdentity wraps a pair of Identity and Manager.
type ManagerIdentity struct {
Identity
Manager
}
func (p ManagerIdentity) String() string {
return fmt.Sprintf("%s:%s", p.Identity.IdentifierBase(), p.Manager.IdentifierBase())
}
type searchID struct {
id Identity
isDp bool
isPeq bool
hasEqer bool
maxDepth int
seen map[Manager]bool
dp IsProbablyDependentProvider
peq compare.ProbablyEqer
eqer compare.Eqer
}
// Copyright 2024 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 provides ways to identify values in Hugo. Used for dependency tracking etc.
package identity
import (
"fmt"
"path"
"path/filepath"
"sort"
"strings"
"sync"
"sync/atomic"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/compare"
)
const (
// Anonymous is an Identity that can be used when identity doesn't matter.
Anonymous = StringIdentity("__anonymous")
// GenghisKhan is an Identity everyone relates to.
GenghisKhan = StringIdentity("__genghiskhan")
StructuralChangeAdd = StringIdentity("__structural_change_add")
StructuralChangeRemove = StringIdentity("__structural_change_remove")
)
var NopManager = new(nopManager)
// NewIdentityManager creates a new Manager.
func NewManager(name string, opts ...ManagerOption) Manager {
idm := &identityManager{
Identity: Anonymous,
name: name,
ids: Identities{},
}
for _, o := range opts {
o(idm)
}
return idm
}
// CleanString cleans s to be suitable as an identifier.
func CleanString(s string) string {
s = strings.ToLower(s)
s = strings.Trim(filepath.ToSlash(s), "/")
return "/" + path.Clean(s)
}
// CleanStringIdentity cleans s to be suitable as an identifier and wraps it in a StringIdentity.
func CleanStringIdentity(s string) StringIdentity {
return StringIdentity(CleanString(s))
}
// GetDependencyManager returns the DependencyManager from v or nil if none found.
func GetDependencyManager(v any) Manager {
switch vv := v.(type) {
case Manager:
return vv
case types.Unwrapper:
return GetDependencyManager(vv.Unwrapv())
case DependencyManagerProvider:
return vv.GetDependencyManager()
}
return nil
}
// FirstIdentity returns the first Identity in v, Anonymous if none found
func FirstIdentity(v any) Identity {
var result Identity = Anonymous
WalkIdentitiesShallow(v, func(level int, id Identity) bool {
result = id
return result != Anonymous
})
return result
}
// PrintIdentityInfo is used for debugging/tests only.
func PrintIdentityInfo(v any) {
WalkIdentitiesDeep(v, func(level int, id Identity) bool {
var s string
if idm, ok := id.(*identityManager); ok {
s = " " + idm.name
}
fmt.Printf("%s%s (%T)%s\n", strings.Repeat(" ", level), id.IdentifierBase(), id, s)
return false
})
}
func Unwrap(id Identity) Identity {
switch t := id.(type) {
case IdentityProvider:
return t.GetIdentity()
default:
return id
}
}
// WalkIdentitiesDeep walks identities in v and applies cb to every identity found.
// Return true from cb to terminate.
// If deep is true, it will also walk nested Identities in any Manager found.
func WalkIdentitiesDeep(v any, cb func(level int, id Identity) bool) {
seen := make(map[Identity]bool)
walkIdentities(v, 0, true, seen, cb)
}
// WalkIdentitiesShallow will not walk into a Manager's Identities.
// See WalkIdentitiesDeep.
// cb is called for every Identity found and returns whether to terminate the walk.
func WalkIdentitiesShallow(v any, cb func(level int, id Identity) bool) {
walkIdentitiesShallow(v, 0, cb)
}
// WithOnAddIdentity sets a callback that will be invoked when an identity is added to the manager.
func WithOnAddIdentity(f func(id Identity)) ManagerOption {
return func(m *identityManager) {
m.onAddIdentity = f
}
}
// DependencyManagerProvider provides a manager for dependencies.
type DependencyManagerProvider interface {
GetDependencyManager() Manager
}
// DependencyManagerProviderFunc is a function that implements the DependencyManagerProvider interface.
type DependencyManagerProviderFunc func() Manager
func (d DependencyManagerProviderFunc) GetDependencyManager() Manager {
return d()
}
// DependencyManagerScopedProvider provides a manager for dependencies with a given scope.
type DependencyManagerScopedProvider interface {
GetDependencyManagerForScope(scope int) Manager
GetDependencyManagerForScopesAll() []Manager
}
// ForEeachIdentityProvider provides a way iterate over identities.
type ForEeachIdentityProvider interface {
// ForEeachIdentityProvider calls cb for each Identity.
// If cb returns true, the iteration is terminated.
// The return value is whether the iteration was terminated.
ForEeachIdentity(cb func(id Identity) bool) bool
}
// ForEeachIdentityProviderFunc is a function that implements the ForEeachIdentityProvider interface.
type ForEeachIdentityProviderFunc func(func(id Identity) bool) bool
func (f ForEeachIdentityProviderFunc) ForEeachIdentity(cb func(id Identity) bool) bool {
return f(cb)
}
// ForEeachIdentityByNameProvider provides a way to look up identities by name.
type ForEeachIdentityByNameProvider interface {
// ForEeachIdentityByName calls cb for each Identity that relates to name.
// If cb returns true, the iteration is terminated.
ForEeachIdentityByName(name string, cb func(id Identity) bool)
}
type FindFirstManagerIdentityProvider interface {
Identity
FindFirstManagerIdentity() ManagerIdentity
}
func NewFindFirstManagerIdentityProvider(m Manager, id Identity) FindFirstManagerIdentityProvider {
return findFirstManagerIdentity{
Identity: Anonymous,
ManagerIdentity: ManagerIdentity{
Manager: m, Identity: id,
},
}
}
type findFirstManagerIdentity struct {
Identity
ManagerIdentity
}
func (f findFirstManagerIdentity) FindFirstManagerIdentity() ManagerIdentity {
return f.ManagerIdentity
}
// Identities stores identity providers.
type Identities map[Identity]bool
func (ids Identities) AsSlice() []Identity {
s := make([]Identity, len(ids))
i := 0
for v := range ids {
s[i] = v
i++
}
sort.Slice(s, func(i, j int) bool {
return s[i].IdentifierBase() < s[j].IdentifierBase()
})
return s
}
func (ids Identities) String() string {
var sb strings.Builder
i := 0
for id := range ids {
sb.WriteString(fmt.Sprintf("[%s]", id.IdentifierBase()))
if i < len(ids)-1 {
sb.WriteString(", ")
}
i++
}
return sb.String()
}
// Identity represents a thing in Hugo (a Page, a template etc.)
// Any implementation must be comparable/hashable.
type Identity interface {
IdentifierBase() string
}
// IdentityGroupProvider can be implemented by tightly connected types.
// Current use case is Resource transformation via Hugo Pipes.
type IdentityGroupProvider interface {
GetIdentityGroup() Identity
}
// IdentityProvider can be implemented by types that isn't itself and Identity,
// usually because they're not comparable/hashable.
type IdentityProvider interface {
GetIdentity() Identity
}
// SignalRebuilder is an optional interface for types that can signal a rebuild.
type SignalRebuilder interface {
SignalRebuild(ids ...Identity)
}
// IncrementByOne implements Incrementer adding 1 every time Incr is called.
type IncrementByOne struct {
counter uint64
}
func (c *IncrementByOne) Incr() int {
return int(atomic.AddUint64(&c.counter, uint64(1)))
}
// Incrementer increments and returns the value.
// Typically used for IDs.
type Incrementer interface {
Incr() int
}
// IsProbablyDependentProvider is an optional interface for Identity.
type IsProbablyDependentProvider interface {
IsProbablyDependent(other Identity) bool
}
// IsProbablyDependencyProvider is an optional interface for Identity.
type IsProbablyDependencyProvider interface {
IsProbablyDependency(other Identity) bool
}
// Manager is an Identity that also manages identities, typically dependencies.
type Manager interface {
Identity
AddIdentity(ids ...Identity)
AddIdentityForEach(ids ...ForEeachIdentityProvider)
GetIdentity() Identity
Reset()
forEeachIdentity(func(id Identity) bool) bool
}
type ManagerOption func(m *identityManager)
// StringIdentity is an Identity that wraps a string.
type StringIdentity string
func (s StringIdentity) IdentifierBase() string {
return string(s)
}
type identityManager struct {
Identity
// Only used for debugging.
name string
// mu protects _changes_ to this manager,
// reads currently assumes no concurrent writes.
mu sync.RWMutex
ids Identities
forEachIds []ForEeachIdentityProvider
// Hooks used in debugging.
onAddIdentity func(id Identity)
}
func (im *identityManager) AddIdentity(ids ...Identity) {
im.mu.Lock()
defer im.mu.Unlock()
for _, id := range ids {
if id == nil || id == Anonymous {
continue
}
if _, found := im.ids[id]; !found {
if im.onAddIdentity != nil {
im.onAddIdentity(id)
}
im.ids[id] = true
}
}
}
func (im *identityManager) AddIdentityForEach(ids ...ForEeachIdentityProvider) {
im.mu.Lock()
im.forEachIds = append(im.forEachIds, ids...)
im.mu.Unlock()
}
func (im *identityManager) ContainsIdentity(id Identity) FinderResult {
if im.Identity != Anonymous && id == im.Identity {
return FinderFound
}
f := NewFinder(FinderConfig{Exact: true})
r := f.Contains(id, im, -1)
return r
}
// Managers are always anonymous.
func (im *identityManager) GetIdentity() Identity {
return im.Identity
}
func (im *identityManager) Reset() {
im.mu.Lock()
im.ids = Identities{}
im.mu.Unlock()
}
func (im *identityManager) GetDependencyManagerForScope(int) Manager {
return im
}
func (im *identityManager) GetDependencyManagerForScopesAll() []Manager {
return []Manager{im}
}
func (im *identityManager) String() string {
return fmt.Sprintf("IdentityManager(%s)", im.name)
}
func (im *identityManager) forEeachIdentity(fn func(id Identity) bool) bool {
// The absence of a lock here is deliberate. This is currently only used on server reloads
// in a single-threaded context.
for id := range im.ids {
if fn(id) {
return true
}
}
for _, fe := range im.forEachIds {
if fe.ForEeachIdentity(fn) {
return true
}
}
return false
}
type nopManager int
func (m *nopManager) AddIdentity(ids ...Identity) {
}
func (m *nopManager) AddIdentityForEach(ids ...ForEeachIdentityProvider) {
}
func (m *nopManager) IdentifierBase() string {
return ""
}
func (m *nopManager) GetIdentity() Identity {
return Anonymous
}
func (m *nopManager) Reset() {
}
func (m *nopManager) forEeachIdentity(func(id Identity) bool) bool {
return false
}
// returns whether further walking should be terminated.
func walkIdentities(v any, level int, deep bool, seen map[Identity]bool, cb func(level int, id Identity) bool) {
if level > 20 {
panic("too deep")
}
var cbRecursive func(level int, id Identity) bool
cbRecursive = func(level int, id Identity) bool {
if id == nil {
return false
}
if deep && seen[id] {
return false
}
seen[id] = true
if cb(level, id) {
return true
}
if deep {
if m := GetDependencyManager(id); m != nil {
m.forEeachIdentity(func(id2 Identity) bool {
return walkIdentitiesShallow(id2, level+1, cbRecursive)
})
}
}
return false
}
walkIdentitiesShallow(v, level, cbRecursive)
}
// returns whether further walking should be terminated.
// Anonymous identities are skipped.
func walkIdentitiesShallow(v any, level int, cb func(level int, id Identity) bool) bool {
cb2 := func(level int, id Identity) bool {
if id == Anonymous {
return false
}
if id == nil {
return false
}
return cb(level, id)
}
if id, ok := v.(Identity); ok {
if cb2(level, id) {
return true
}
}
if ipd, ok := v.(IdentityProvider); ok {
if cb2(level, ipd.GetIdentity()) {
return true
}
}
if ipdgp, ok := v.(IdentityGroupProvider); ok {
if cb2(level, ipdgp.GetIdentityGroup()) {
return true
}
}
return false
}
var (
_ Identity = (*orIdentity)(nil)
_ compare.ProbablyEqer = (*orIdentity)(nil)
)
func Or(a, b Identity) Identity {
return orIdentity{a: a, b: b}
}
type orIdentity struct {
a, b Identity
}
func (o orIdentity) IdentifierBase() string {
return o.a.IdentifierBase()
}
func (o orIdentity) ProbablyEq(other any) bool {
otherID, ok := other.(Identity)
if !ok {
return false
}
return probablyEq(o.a, otherID) || probablyEq(o.b, otherID)
}
func probablyEq(a, b Identity) bool {
if a == b {
return true
}
if a == Anonymous || b == Anonymous {
return false
}
if a.IdentifierBase() == b.IdentifierBase() {
return true
}
if a2, ok := a.(compare.ProbablyEqer); ok && a2.ProbablyEq(b) {
return true
}
if a2, ok := a.(IsProbablyDependentProvider); ok {
return a2.IsProbablyDependent(b)
}
return false
}
// Copyright 2024 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 provides ways to identify values in Hugo. Used for dependency tracking etc.
package identity
import (
"fmt"
"sync/atomic"
hglob "github.com/gohugoio/hugo/hugofs/glob"
)
// NewGlobIdentity creates a new Identity that
// is probably dependent on any other Identity
// that matches the given pattern.
func NewGlobIdentity(pattern string) Identity {
glob, err := hglob.GetGlob(pattern)
if err != nil {
panic(err)
}
predicate := func(other Identity) bool {
return glob.Match(other.IdentifierBase())
}
return NewPredicateIdentity(predicate, nil)
}
var predicateIdentityCounter = &atomic.Uint32{}
type predicateIdentity struct {
id string
probablyDependent func(Identity) bool
probablyDependency func(Identity) bool
}
var (
_ IsProbablyDependencyProvider = &predicateIdentity{}
_ IsProbablyDependentProvider = &predicateIdentity{}
)
// NewPredicateIdentity creates a new Identity that implements both IsProbablyDependencyProvider and IsProbablyDependentProvider
// using the provided functions, both of which are optional.
func NewPredicateIdentity(
probablyDependent func(Identity) bool,
probablyDependency func(Identity) bool,
) *predicateIdentity {
if probablyDependent == nil {
probablyDependent = func(Identity) bool { return false }
}
if probablyDependency == nil {
probablyDependency = func(Identity) bool { return false }
}
return &predicateIdentity{probablyDependent: probablyDependent, probablyDependency: probablyDependency, id: fmt.Sprintf("predicate%d", predicateIdentityCounter.Add(1))}
}
func (id *predicateIdentity) IdentifierBase() string {
return id.id
}
func (id *predicateIdentity) IsProbablyDependent(other Identity) bool {
return id.probablyDependent(other)
}
func (id *predicateIdentity) IsProbablyDependency(other Identity) bool {
return id.probablyDependency(other)
}
// Copyright 2024 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 identity
import "sync"
// NewQuestion creates a new question with the given identity.
func NewQuestion[T any](id Identity) *Question[T] {
return &Question[T]{
Identity: id,
}
}
// Answer takes a func that knows the answer.
// Note that this is a one-time operation,
// fn will not be invoked again it the question is already answered.
// Use Result to check if the question is answered.
func (q *Question[T]) Answer(fn func() T) {
q.mu.Lock()
defer q.mu.Unlock()
if q.answered {
return
}
q.fasit = fn()
q.answered = true
}
// Result returns the fasit of the question (if answered),
// and a bool indicating if the question has been answered.
func (q *Question[T]) Result() (any, bool) {
q.mu.RLock()
defer q.mu.RUnlock()
return q.fasit, q.answered
}
// A Question is defined by its Identity and can be answered once.
type Question[T any] struct {
Identity
fasit T
mu sync.RWMutex
answered bool
}
// Copyright 2018 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 metadecoders
import (
"bytes"
"encoding/csv"
"encoding/json"
"fmt"
"log"
"regexp"
"strconv"
"strings"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/maps"
"github.com/niklasfasching/go-org/org"
xml "github.com/clbanning/mxj/v2"
toml "github.com/pelletier/go-toml/v2"
"github.com/spf13/afero"
"github.com/spf13/cast"
yaml "gopkg.in/yaml.v2"
)
// Decoder provides some configuration options for the decoders.
type Decoder struct {
// Delimiter is the field delimiter. Used in the CSV decoder. Default is
// ','.
Delimiter rune
// Comment, if not 0, is the comment character. Lines beginning with the
// Comment character without preceding whitespace are ignored. Used in the
// CSV decoder.
Comment rune
// If true, a quote may appear in an unquoted field and a non-doubled quote
// may appear in a quoted field. Used in the CSV decoder. Default is false.
LazyQuotes bool
// The target data type, either slice or map. Used in the CSV decoder.
// Default is slice.
TargetType string
}
// OptionsKey is used in cache keys.
func (d Decoder) OptionsKey() string {
var sb strings.Builder
sb.WriteRune(d.Delimiter)
sb.WriteRune(d.Comment)
sb.WriteString(strconv.FormatBool(d.LazyQuotes))
sb.WriteString(d.TargetType)
return sb.String()
}
// Default is a Decoder in its default configuration.
var Default = Decoder{
Delimiter: ',',
TargetType: "slice",
}
// UnmarshalToMap will unmarshall data in format f into a new map. This is
// what's needed for Hugo's front matter decoding.
func (d Decoder) UnmarshalToMap(data []byte, f Format) (map[string]any, error) {
m := make(map[string]any)
if data == nil {
return m, nil
}
err := d.UnmarshalTo(data, f, &m)
return m, err
}
// UnmarshalFileToMap is the same as UnmarshalToMap, but reads the data from
// the given filename.
func (d Decoder) UnmarshalFileToMap(fs afero.Fs, filename string) (map[string]any, error) {
format := FormatFromString(filename)
if format == "" {
return nil, fmt.Errorf("%q is not a valid configuration format", filename)
}
data, err := afero.ReadFile(fs, filename)
if err != nil {
return nil, err
}
return d.UnmarshalToMap(data, format)
}
// UnmarshalStringTo tries to unmarshal data to a new instance of type typ.
func (d Decoder) UnmarshalStringTo(data string, typ any) (any, error) {
data = strings.TrimSpace(data)
// We only check for the possible types in YAML, JSON and TOML.
switch typ.(type) {
case string:
return data, nil
case map[string]any, maps.Params:
format := d.FormatFromContentString(data)
return d.UnmarshalToMap([]byte(data), format)
case []any:
// A standalone slice. Let YAML handle it.
return d.Unmarshal([]byte(data), YAML)
case bool:
return cast.ToBoolE(data)
case int:
return cast.ToIntE(data)
case int64:
return cast.ToInt64E(data)
case float64:
return cast.ToFloat64E(data)
default:
return nil, fmt.Errorf("unmarshal: %T not supported", typ)
}
}
// Unmarshal will unmarshall data in format f into an interface{}.
// This is what's needed for Hugo's /data handling.
func (d Decoder) Unmarshal(data []byte, f Format) (any, error) {
if len(data) == 0 {
switch f {
case CSV:
switch d.TargetType {
case "map":
return make(map[string]any), nil
case "slice":
return make([][]string, 0), nil
default:
return nil, fmt.Errorf("invalid targetType: expected either slice or map, received %s", d.TargetType)
}
default:
return make(map[string]any), nil
}
}
var v any
err := d.UnmarshalTo(data, f, &v)
return v, err
}
// UnmarshalTo unmarshals data in format f into v.
func (d Decoder) UnmarshalTo(data []byte, f Format, v any) error {
var err error
switch f {
case ORG:
err = d.unmarshalORG(data, v)
case JSON:
err = json.Unmarshal(data, v)
case XML:
var xmlRoot xml.Map
xmlRoot, err = xml.NewMapXml(data)
var xmlValue map[string]any
if err == nil {
xmlRootName, err := xmlRoot.Root()
if err != nil {
return toFileError(f, data, fmt.Errorf("failed to unmarshal XML: %w", err))
}
// Get the root value and verify it's a map
rootValue := xmlRoot[xmlRootName]
if rootValue == nil {
return toFileError(f, data, fmt.Errorf("XML root element '%s' has no value", xmlRootName))
}
// Type check before conversion
mapValue, ok := rootValue.(map[string]any)
if !ok {
return toFileError(f, data, fmt.Errorf("XML root element '%s' must be a map/object, got %T", xmlRootName, rootValue))
}
xmlValue = mapValue
}
switch v := v.(type) {
case *map[string]any:
*v = xmlValue
case *any:
*v = xmlValue
}
case TOML:
err = toml.Unmarshal(data, v)
case YAML:
err = yaml.Unmarshal(data, v)
if err != nil {
return toFileError(f, data, fmt.Errorf("failed to unmarshal YAML: %w", err))
}
// To support boolean keys, the YAML package unmarshals maps to
// map[interface{}]interface{}. Here we recurse through the result
// and change all maps to map[string]interface{} like we would've
// gotten from `json`.
var ptr any
switch vv := v.(type) {
case *map[string]any:
ptr = *vv
case *any:
ptr = *vv
default:
// Not a map.
}
if ptr != nil {
if mm, changed := stringifyMapKeys(ptr); changed {
switch vv := v.(type) {
case *map[string]any:
*vv = mm.(map[string]any)
case *any:
*vv = mm
}
}
}
case CSV:
return d.unmarshalCSV(data, v)
default:
return fmt.Errorf("unmarshal of format %q is not supported", f)
}
if err == nil {
return nil
}
return toFileError(f, data, fmt.Errorf("unmarshal failed: %w", err))
}
func (d Decoder) unmarshalCSV(data []byte, v any) error {
r := csv.NewReader(bytes.NewReader(data))
r.Comma = d.Delimiter
r.Comment = d.Comment
r.LazyQuotes = d.LazyQuotes
records, err := r.ReadAll()
if err != nil {
return err
}
switch vv := v.(type) {
case *any:
switch d.TargetType {
case "map":
if len(records) < 2 {
return fmt.Errorf("cannot unmarshal CSV into %T: expected at least a header row and one data row", v)
}
seen := make(map[string]bool, len(records[0]))
for _, fieldName := range records[0] {
if seen[fieldName] {
return fmt.Errorf("cannot unmarshal CSV into %T: header row contains duplicate field names", v)
}
seen[fieldName] = true
}
sm := make([]map[string]string, len(records)-1)
for i, record := range records[1:] {
m := make(map[string]string, len(records[0]))
for j, col := range record {
m[records[0][j]] = col
}
sm[i] = m
}
*vv = sm
case "slice":
*vv = records
default:
return fmt.Errorf("cannot unmarshal CSV into %T: invalid targetType: expected either slice or map, received %s", v, d.TargetType)
}
default:
return fmt.Errorf("cannot unmarshal CSV into %T", v)
}
return nil
}
func parseORGDate(s string) string {
r := regexp.MustCompile(`[<\[](\d{4}-\d{2}-\d{2}) .*[>\]]`)
if m := r.FindStringSubmatch(s); m != nil {
return m[1]
}
return s
}
func (d Decoder) unmarshalORG(data []byte, v any) error {
config := org.New()
config.Log = log.Default() // TODO(bep)
document := config.Parse(bytes.NewReader(data), "")
if document.Error != nil {
return document.Error
}
frontMatter := make(map[string]any, len(document.BufferSettings))
for k, v := range document.BufferSettings {
k = strings.ToLower(k)
if strings.HasSuffix(k, "[]") {
frontMatter[k[:len(k)-2]] = strings.Fields(v)
} else if strings.Contains(v, "\n") {
frontMatter[k] = strings.Split(v, "\n")
} else if k == "filetags" {
trimmed := strings.TrimPrefix(v, ":")
trimmed = strings.TrimSuffix(trimmed, ":")
frontMatter[k] = strings.Split(trimmed, ":")
} else if k == "date" || k == "lastmod" || k == "publishdate" || k == "expirydate" {
frontMatter[k] = parseORGDate(v)
} else {
frontMatter[k] = v
}
}
switch vv := v.(type) {
case *map[string]any:
*vv = frontMatter
case *any:
*vv = frontMatter
}
return nil
}
func toFileError(f Format, data []byte, err error) error {
return herrors.NewFileErrorFromName(err, fmt.Sprintf("_stream.%s", f)).UpdateContent(bytes.NewReader(data), nil)
}
// stringifyMapKeys recurses into in and changes all instances of
// map[interface{}]interface{} to map[string]interface{}. This is useful to
// work around the impedance mismatch between JSON and YAML unmarshaling that's
// described here: https://github.com/go-yaml/yaml/issues/139
//
// Inspired by https://github.com/stripe/stripe-mock, MIT licensed
func stringifyMapKeys(in any) (any, bool) {
switch in := in.(type) {
case []any:
for i, v := range in {
if vv, replaced := stringifyMapKeys(v); replaced {
in[i] = vv
}
}
case map[string]any:
for k, v := range in {
if vv, changed := stringifyMapKeys(v); changed {
in[k] = vv
}
}
case map[any]any:
res := make(map[string]any)
var (
ok bool
err error
)
for k, v := range in {
var ks string
if ks, ok = k.(string); !ok {
ks, err = cast.ToStringE(k)
if err != nil {
ks = fmt.Sprintf("%v", k)
}
}
if vv, replaced := stringifyMapKeys(v); replaced {
res[ks] = vv
} else {
res[ks] = v
}
}
return res, true
}
return nil, false
}
// Copyright 2018 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 metadecoders
import (
"path/filepath"
"strings"
)
type Format string
const (
// These are the supported metadata formats in Hugo. Most of these are also
// supported as /data formats.
ORG Format = "org"
JSON Format = "json"
TOML Format = "toml"
YAML Format = "yaml"
CSV Format = "csv"
XML Format = "xml"
)
// FormatFromStrings returns the first non-empty Format from the given strings.
func FormatFromStrings(ss ...string) Format {
for _, s := range ss {
if f := FormatFromString(s); f != "" {
return f
}
}
return ""
}
// FormatFromString turns formatStr, typically a file extension without any ".",
// into a Format. It returns an empty string for unknown formats.
func FormatFromString(formatStr string) Format {
formatStr = strings.ToLower(formatStr)
if strings.Contains(formatStr, ".") {
// Assume a filename
formatStr = strings.TrimPrefix(filepath.Ext(formatStr), ".")
}
switch formatStr {
case "yaml", "yml":
return YAML
case "json":
return JSON
case "toml":
return TOML
case "org":
return ORG
case "csv":
return CSV
case "xml":
return XML
}
return ""
}
// FormatFromContentString tries to detect the format (JSON, YAML, TOML or XML)
// in the given string.
// It return an empty string if no format could be detected.
func (d Decoder) FormatFromContentString(data string) Format {
csvIdx := strings.IndexRune(data, d.Delimiter)
jsonIdx := strings.Index(data, "{")
yamlIdx := strings.Index(data, ":")
xmlIdx := strings.Index(data, "<")
tomlIdx := strings.Index(data, "=")
if isLowerIndexThan(csvIdx, jsonIdx, yamlIdx, xmlIdx, tomlIdx) {
return CSV
}
if isLowerIndexThan(jsonIdx, yamlIdx, xmlIdx, tomlIdx) {
return JSON
}
if isLowerIndexThan(yamlIdx, xmlIdx, tomlIdx) {
return YAML
}
if isLowerIndexThan(xmlIdx, tomlIdx) {
return XML
}
if tomlIdx != -1 {
return TOML
}
return ""
}
func isLowerIndexThan(first int, others ...int) bool {
if first == -1 {
return false
}
for _, other := range others {
if other != -1 && other < first {
return false
}
}
return true
}
// Copyright 2025 Google LLC
//
// 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 pageparser
import "bytes"
func FuzzParseFrontMatterAndContent(data []byte) int {
ParseFrontMatterAndContent(bytes.NewReader(data))
return 1
}
// Copyright 2018 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 pageparser
import (
"bytes"
"fmt"
"regexp"
"strconv"
"github.com/yuin/goldmark/util"
)
type lowHigh struct {
Low int
High int
}
type Item struct {
Type ItemType
Err error
// The common case is a single segment.
low int
high int
// This is the uncommon case.
segments []lowHigh
// Used for validation.
firstByte byte
isString bool
}
type Items []Item
func (i Item) Pos() int {
if len(i.segments) > 0 {
return i.segments[0].Low
}
return i.low
}
func (i Item) Val(source []byte) []byte {
if len(i.segments) == 0 {
return source[i.low:i.high]
}
if len(i.segments) == 1 {
return source[i.segments[0].Low:i.segments[0].High]
}
var b bytes.Buffer
for _, s := range i.segments {
b.Write(source[s.Low:s.High])
}
return b.Bytes()
}
func (i Item) ValStr(source []byte) string {
return string(i.Val(source))
}
func (i Item) ValTyped(source []byte) any {
str := i.ValStr(source)
if i.isString {
// A quoted value that is a string even if it looks like a number etc.
return str
}
if boolRe.MatchString(str) {
return str == "true"
}
if intRe.MatchString(str) {
num, err := strconv.Atoi(str)
if err != nil {
return str
}
return num
}
if floatRe.MatchString(str) {
num, err := strconv.ParseFloat(str, 64)
if err != nil {
return str
}
return num
}
return str
}
func (i Item) IsText() bool {
return i.Type == tText || i.IsIndentation()
}
func (i Item) IsIndentation() bool {
return i.Type == tIndentation
}
func (i Item) IsNonWhitespace(source []byte) bool {
return len(bytes.TrimSpace(i.Val(source))) > 0
}
func (i Item) IsShortcodeName() bool {
return i.Type == tScName
}
func (i Item) IsInlineShortcodeName() bool {
return i.Type == tScNameInline
}
func (i Item) IsLeftShortcodeDelim() bool {
return i.Type == tLeftDelimScWithMarkup || i.Type == tLeftDelimScNoMarkup
}
func (i Item) IsRightShortcodeDelim() bool {
return i.Type == tRightDelimScWithMarkup || i.Type == tRightDelimScNoMarkup
}
func (i Item) IsShortcodeClose() bool {
return i.Type == tScClose
}
func (i Item) IsShortcodeParam() bool {
return i.Type == tScParam
}
func (i Item) IsShortcodeParamVal() bool {
return i.Type == tScParamVal
}
func (i Item) IsShortcodeMarkupDelimiter() bool {
return i.Type == tLeftDelimScWithMarkup || i.Type == tRightDelimScWithMarkup
}
func (i Item) IsFrontMatter() bool {
return i.Type >= TypeFrontMatterYAML && i.Type <= TypeFrontMatterORG
}
func (i Item) IsDone() bool {
return i.IsError() || i.IsEOF()
}
func (i Item) IsEOF() bool {
return i.Type == tEOF
}
func (i Item) IsError() bool {
return i.Type == tError
}
func (i Item) ToString(source []byte) string {
val := i.Val(source)
switch {
case i.IsEOF():
return "EOF"
case i.IsError():
return string(val)
case i.IsIndentation():
return fmt.Sprintf("%s:[%s]", i.Type, util.VisualizeSpaces(val))
case i.Type > tKeywordMarker:
return fmt.Sprintf("<%s>", val)
case len(val) > 50:
return fmt.Sprintf("%v:%.20q...", i.Type, val)
default:
return fmt.Sprintf("%v:[%s]", i.Type, val)
}
}
type ItemType int
const (
tError ItemType = iota
tEOF
// page items
TypeLeadSummaryDivider // <!--more-->, # more
TypeFrontMatterYAML
TypeFrontMatterTOML
TypeFrontMatterJSON
TypeFrontMatterORG
TypeIgnore // // The BOM Unicode byte order marker and possibly others
// shortcode items
tLeftDelimScNoMarkup
tRightDelimScNoMarkup
tLeftDelimScWithMarkup
tRightDelimScWithMarkup
tScClose
tScName
tScNameInline
tScParam
tScParamVal
tIndentation
tText // plain text
// preserved for later - keywords come after this
tKeywordMarker
)
var (
boolRe = regexp.MustCompile(`^(true|false)$`)
intRe = regexp.MustCompile(`^[-+]?\d+$`)
floatRe = regexp.MustCompile(`^[-+]?\d*\.\d+$`)
)
// Code generated by "stringer -type ItemType"; DO NOT EDIT.
package pageparser
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[tError-0]
_ = x[tEOF-1]
_ = x[TypeLeadSummaryDivider-2]
_ = x[TypeFrontMatterYAML-3]
_ = x[TypeFrontMatterTOML-4]
_ = x[TypeFrontMatterJSON-5]
_ = x[TypeFrontMatterORG-6]
_ = x[TypeIgnore-7]
_ = x[tLeftDelimScNoMarkup-8]
_ = x[tRightDelimScNoMarkup-9]
_ = x[tLeftDelimScWithMarkup-10]
_ = x[tRightDelimScWithMarkup-11]
_ = x[tScClose-12]
_ = x[tScName-13]
_ = x[tScNameInline-14]
_ = x[tScParam-15]
_ = x[tScParamVal-16]
_ = x[tIndentation-17]
_ = x[tText-18]
_ = x[tKeywordMarker-19]
}
const _ItemType_name = "tErrortEOFTypeLeadSummaryDividerTypeFrontMatterYAMLTypeFrontMatterTOMLTypeFrontMatterJSONTypeFrontMatterORGTypeIgnoretLeftDelimScNoMarkuptRightDelimScNoMarkuptLeftDelimScWithMarkuptRightDelimScWithMarkuptScClosetScNametScNameInlinetScParamtScParamValtIndentationtTexttKeywordMarker"
var _ItemType_index = [...]uint16{0, 6, 10, 32, 51, 70, 89, 107, 117, 137, 158, 180, 203, 211, 218, 231, 239, 250, 262, 267, 281}
func (i ItemType) String() string {
if i < 0 || i >= ItemType(len(_ItemType_index)-1) {
return "ItemType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ItemType_name[_ItemType_index[i]:_ItemType_index[i+1]]
}
// Copyright 2018 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 pageparser
import (
"bytes"
"fmt"
"unicode"
"unicode/utf8"
)
const eof = -1
// returns the next state in scanner.
type stateFunc func(*pageLexer) stateFunc
type pageLexer struct {
input []byte
stateStart stateFunc
state stateFunc
pos int // input position
start int // item start position
width int // width of last element
// Contains lexers for shortcodes and other main section
// elements.
sectionHandlers *sectionHandlers
cfg Config
// The summary divider to look for.
summaryDivider []byte
// Set when we have parsed any summary divider
summaryDividerChecked bool
lexerShortcodeState
// items delivered to client
items Items
// error delivered to the client
err error
}
// Implement the Result interface
func (l *pageLexer) Iterator() *Iterator {
return NewIterator(l.items)
}
func (l *pageLexer) Input() []byte {
return l.input
}
type Config struct {
NoFrontMatter bool
NoSummaryDivider bool
}
// note: the input position here is normally 0 (start), but
// can be set if position of first shortcode is known
func newPageLexer(input []byte, stateStart stateFunc, cfg Config) *pageLexer {
lexer := &pageLexer{
input: input,
stateStart: stateStart,
summaryDivider: summaryDivider,
cfg: cfg,
lexerShortcodeState: lexerShortcodeState{
currLeftDelimItem: tLeftDelimScNoMarkup,
currRightDelimItem: tRightDelimScNoMarkup,
openShortcodes: make(map[string]bool),
},
items: make([]Item, 0, 5),
}
lexer.sectionHandlers = createSectionHandlers(lexer)
return lexer
}
// main loop
func (l *pageLexer) run() *pageLexer {
for l.state = l.stateStart; l.state != nil; {
l.state = l.state(l)
}
return l
}
// Page syntax
var (
byteOrderMark = '\ufeff'
summaryDivider = []byte("<!--more-->")
summaryDividerOrg = []byte("# more")
delimTOML = []byte("+++")
delimYAML = []byte("---")
delimOrg = []byte("#+")
)
func (l *pageLexer) next() rune {
if l.pos >= len(l.input) {
l.width = 0
return eof
}
runeValue, runeWidth := utf8.DecodeRune(l.input[l.pos:])
l.width = runeWidth
l.pos += l.width
return runeValue
}
// peek, but no consume
func (l *pageLexer) peek() rune {
r := l.next()
l.backup()
return r
}
// steps back one
func (l *pageLexer) backup() {
l.pos -= l.width
}
func (l *pageLexer) append(item Item) {
if item.Pos() < len(l.input) {
item.firstByte = l.input[item.Pos()]
}
l.items = append(l.items, item)
}
// sends an item back to the client.
func (l *pageLexer) emit(t ItemType) {
defer func() {
l.start = l.pos
}()
if t == tText {
// Identify any trailing whitespace/intendation.
// We currently only care about the last one.
for i := l.pos - 1; i >= l.start; i-- {
b := l.input[i]
if b != ' ' && b != '\t' && b != '\r' && b != '\n' {
break
}
if i == l.start && b != '\n' {
l.append(Item{Type: tIndentation, low: l.start, high: l.pos})
return
} else if b == '\n' && i < l.pos-1 {
l.append(Item{Type: t, low: l.start, high: i + 1})
l.append(Item{Type: tIndentation, low: i + 1, high: l.pos})
return
} else if b == '\n' && i == l.pos-1 {
break
}
}
}
l.append(Item{Type: t, low: l.start, high: l.pos})
}
// sends a string item back to the client.
func (l *pageLexer) emitString(t ItemType) {
l.append(Item{Type: t, low: l.start, high: l.pos, isString: true})
l.start = l.pos
}
func (l *pageLexer) isEOF() bool {
return l.pos >= len(l.input)
}
// special case, do not send '\\' back to client
func (l *pageLexer) ignoreEscapesAndEmit(t ItemType, isString bool) {
i := l.start
k := i
var segments []lowHigh
for i < l.pos {
r, w := utf8.DecodeRune(l.input[i:l.pos])
if r == '\\' {
if i > k {
segments = append(segments, lowHigh{k, i})
}
// See issue #10236.
// We don't send the backslash back to the client,
// which makes the end parsing simpler.
// This means that we cannot render the AST back to be
// exactly the same as the input,
// but that was also the situation before we introduced the issue in #10236.
k = i + w
}
i += w
}
if k < l.pos {
segments = append(segments, lowHigh{k, l.pos})
}
if len(segments) > 0 {
l.append(Item{Type: t, segments: segments})
}
l.start = l.pos
}
// gets the current value (for debugging and error handling)
func (l *pageLexer) current() []byte {
return l.input[l.start:l.pos]
}
// ignore current element
func (l *pageLexer) ignore() {
l.start = l.pos
}
var lf = []byte("\n")
// nil terminates the parser
func (l *pageLexer) errorf(format string, args ...any) stateFunc {
l.append(Item{Type: tError, Err: fmt.Errorf(format, args...), low: l.start, high: l.pos})
return nil
}
func (l *pageLexer) consumeCRLF() bool {
var consumed bool
for _, r := range crLf {
if l.next() != r {
l.backup()
} else {
consumed = true
}
}
return consumed
}
func (l *pageLexer) consumeToSpace() {
for {
r := l.next()
if r == eof || unicode.IsSpace(r) {
l.backup()
return
}
}
}
func (l *pageLexer) consumeSpace() {
for {
r := l.next()
if r == eof || !unicode.IsSpace(r) {
l.backup()
return
}
}
}
type sectionHandlers struct {
l *pageLexer
// Set when none of the sections are found so we
// can safely stop looking and skip to the end.
skipAll bool
handlers []*sectionHandler
skipIndexes []int
}
func (s *sectionHandlers) skip() int {
if s.skipAll {
return -1
}
s.skipIndexes = s.skipIndexes[:0]
var shouldSkip bool
for _, skipper := range s.handlers {
idx := skipper.skip()
if idx != -1 {
shouldSkip = true
s.skipIndexes = append(s.skipIndexes, idx)
}
}
if !shouldSkip {
s.skipAll = true
return -1
}
return minIndex(s.skipIndexes...)
}
func createSectionHandlers(l *pageLexer) *sectionHandlers {
handlers := make([]*sectionHandler, 0, 2)
shortCodeHandler := §ionHandler{
l: l,
skipFunc: func(l *pageLexer) int {
return l.index(leftDelimSc)
},
lexFunc: func(origin stateFunc, l *pageLexer) (stateFunc, bool) {
if !l.isShortCodeStart() {
return origin, false
}
if l.isInline {
// If we're inside an inline shortcode, the only valid shortcode markup is
// the markup which closes it.
b := l.input[l.pos+3:]
end := indexNonWhiteSpace(b, '/')
if end != len(l.input)-1 {
b = bytes.TrimSpace(b[end+1:])
if end == -1 || !bytes.HasPrefix(b, []byte(l.currShortcodeName+" ")) {
return l.errorf("inline shortcodes do not support nesting"), true
}
}
}
if l.hasPrefix(leftDelimScWithMarkup) {
l.currLeftDelimItem = tLeftDelimScWithMarkup
l.currRightDelimItem = tRightDelimScWithMarkup
} else {
l.currLeftDelimItem = tLeftDelimScNoMarkup
l.currRightDelimItem = tRightDelimScNoMarkup
}
return lexShortcodeLeftDelim, true
},
}
handlers = append(handlers, shortCodeHandler)
if !l.cfg.NoSummaryDivider {
summaryDividerHandler := §ionHandler{
l: l,
skipFunc: func(l *pageLexer) int {
if l.summaryDividerChecked {
return -1
}
return l.index(l.summaryDivider)
},
lexFunc: func(origin stateFunc, l *pageLexer) (stateFunc, bool) {
if !l.hasPrefix(l.summaryDivider) {
return origin, false
}
l.summaryDividerChecked = true
l.pos += len(l.summaryDivider)
// This makes it a little easier to reason about later.
l.consumeSpace()
l.emit(TypeLeadSummaryDivider)
return origin, true
},
}
handlers = append(handlers, summaryDividerHandler)
}
return §ionHandlers{
l: l,
handlers: handlers,
skipIndexes: make([]int, len(handlers)),
}
}
func (s *sectionHandlers) lex(origin stateFunc) stateFunc {
if s.skipAll {
return nil
}
if s.l.pos > s.l.start {
s.l.emit(tText)
}
for _, handler := range s.handlers {
if handler.skipAll {
continue
}
next, handled := handler.lexFunc(origin, handler.l)
if next == nil || handled {
return next
}
}
// Not handled by the above.
s.l.pos++
return origin
}
type sectionHandler struct {
l *pageLexer
// No more sections of this type.
skipAll bool
// Returns the index of the next match, -1 if none found.
skipFunc func(l *pageLexer) int
// Lex lexes the current section and returns the next state func and
// a bool telling if this section was handled.
// Note that returning nil as the next state will terminate the
// lexer.
lexFunc func(origin stateFunc, l *pageLexer) (stateFunc, bool)
}
func (s *sectionHandler) skip() int {
if s.skipAll {
return -1
}
idx := s.skipFunc(s.l)
if idx == -1 {
s.skipAll = true
}
return idx
}
func lexMainSection(l *pageLexer) stateFunc {
if l.isEOF() {
return lexDone
}
// Fast forward as far as possible.
skip := l.sectionHandlers.skip()
if skip == -1 {
l.pos = len(l.input)
return lexDone
} else if skip > 0 {
l.pos += skip
}
next := l.sectionHandlers.lex(lexMainSection)
if next != nil {
return next
}
l.pos = len(l.input)
return lexDone
}
func lexDone(l *pageLexer) stateFunc {
// Done!
if l.pos > l.start {
l.emit(tText)
}
l.emit(tEOF)
return nil
}
//lint:ignore U1000 useful for debugging
func (l *pageLexer) printCurrentInput() {
fmt.Printf("input[%d:]: %q", l.pos, string(l.input[l.pos:]))
}
// state helpers
func (l *pageLexer) index(sep []byte) int {
return bytes.Index(l.input[l.pos:], sep)
}
func (l *pageLexer) hasPrefix(prefix []byte) bool {
return bytes.HasPrefix(l.input[l.pos:], prefix)
}
// helper functions
// returns the min index >= 0
func minIndex(indices ...int) int {
min := -1
for _, j := range indices {
if j < 0 {
continue
}
if min == -1 {
min = j
} else if j < min {
min = j
}
}
return min
}
func indexNonWhiteSpace(s []byte, in rune) int {
idx := bytes.IndexFunc(s, func(r rune) bool {
return !unicode.IsSpace(r)
})
if idx == -1 {
return -1
}
r, _ := utf8.DecodeRune(s[idx:])
if r == in {
return idx
}
return -1
}
func isSpace(r rune) bool {
return r == ' ' || r == '\t'
}
func isAlphaNumericOrHyphen(r rune) bool {
// let unquoted YouTube ids as positional params slip through (they contain hyphens)
return isAlphaNumeric(r) || r == '-'
}
var crLf = []rune{'\r', '\n'}
func isEndOfLine(r rune) bool {
return r == '\r' || r == '\n'
}
func isAlphaNumeric(r rune) bool {
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
}
// Copyright 2018 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 pageparser
func lexIntroSection(l *pageLexer) stateFunc {
LOOP:
for {
r := l.next()
if r == eof {
break
}
switch {
case r == '+':
return l.lexFrontMatterSection(TypeFrontMatterTOML, r, "TOML", delimTOML)
case r == '-':
return l.lexFrontMatterSection(TypeFrontMatterYAML, r, "YAML", delimYAML)
case r == '{':
return lexFrontMatterJSON
case r == '#':
return lexFrontMatterOrgMode
case r == byteOrderMark:
l.emit(TypeIgnore)
case !isSpace(r) && !isEndOfLine(r):
break LOOP
}
}
// Now move on to the shortcodes.
return lexMainSection
}
func lexFrontMatterJSON(l *pageLexer) stateFunc {
// Include the left delimiter
l.backup()
var (
inQuote bool
level int
)
for {
r := l.next()
switch {
case r == eof:
return l.errorf("unexpected EOF parsing JSON front matter")
case r == '{':
if !inQuote {
level++
}
case r == '}':
if !inQuote {
level--
}
case r == '"':
inQuote = !inQuote
case r == '\\':
// This may be an escaped quote. Make sure it's not marked as a
// real one.
l.next()
}
if level == 0 {
break
}
}
l.consumeCRLF()
l.emit(TypeFrontMatterJSON)
return lexMainSection
}
func lexFrontMatterOrgMode(l *pageLexer) stateFunc {
/*
#+TITLE: Test File For chaseadamsio/goorgeous
#+AUTHOR: Chase Adams
#+DESCRIPTION: Just another golang parser for org content!
*/
l.backup()
if !l.hasPrefix(delimOrg) {
return lexMainSection
}
l.summaryDivider = summaryDividerOrg
// Read lines until we no longer see a #+ prefix
LOOP:
for {
r := l.next()
switch {
case r == '\n':
if !l.hasPrefix(delimOrg) {
break LOOP
}
case r == eof:
break LOOP
}
}
l.emit(TypeFrontMatterORG)
return lexMainSection
}
// Handle YAML or TOML front matter.
func (l *pageLexer) lexFrontMatterSection(tp ItemType, delimr rune, name string, delim []byte) stateFunc {
for range 2 {
if r := l.next(); r != delimr {
return l.errorf("invalid %s delimiter", name)
}
}
// Let front matter start at line 1
wasEndOfLine := l.consumeCRLF()
// We don't care about the delimiters.
l.ignore()
var r rune
for {
if !wasEndOfLine {
r = l.next()
if r == eof {
return l.errorf("EOF looking for end %s front matter delimiter", name)
}
}
if wasEndOfLine || isEndOfLine(r) {
if l.hasPrefix(delim) {
l.emit(tp)
l.pos += 3
l.consumeCRLF()
l.ignore()
break
}
}
wasEndOfLine = false
}
return lexMainSection
}
// Copyright 2018 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 pageparser
type lexerShortcodeState struct {
currLeftDelimItem ItemType
currRightDelimItem ItemType
isInline bool
currShortcodeName string // is only set when a shortcode is in opened state
closingState int // > 0 = on its way to be closed
elementStepNum int // step number in element
paramElements int // number of elements (name + value = 2) found first
openShortcodes map[string]bool // set of shortcodes in open state
}
// Shortcode syntax
var (
leftDelimSc = []byte("{{")
leftDelimScNoMarkup = []byte("{{<")
rightDelimScNoMarkup = []byte(">}}")
leftDelimScWithMarkup = []byte("{{%")
rightDelimScWithMarkup = []byte("%}}")
leftComment = []byte("/*") // comments in this context us used to to mark shortcodes as "not really a shortcode"
rightComment = []byte("*/")
)
func (l *pageLexer) isShortCodeStart() bool {
return l.hasPrefix(leftDelimScWithMarkup) || l.hasPrefix(leftDelimScNoMarkup)
}
func lexShortcodeLeftDelim(l *pageLexer) stateFunc {
l.pos += len(l.currentLeftShortcodeDelim())
if l.hasPrefix(leftComment) {
return lexShortcodeComment
}
l.emit(l.currentLeftShortcodeDelimItem())
l.elementStepNum = 0
l.paramElements = 0
return lexInsideShortcode
}
func lexShortcodeComment(l *pageLexer) stateFunc {
posRightComment := l.index(append(rightComment, l.currentRightShortcodeDelim()...))
if posRightComment <= 1 {
return l.errorf("comment must be closed")
}
// we emit all as text, except the comment markers
l.emit(tText)
l.pos += len(leftComment)
l.ignore()
l.pos += posRightComment - len(leftComment)
l.emit(tText)
l.pos += len(rightComment)
l.ignore()
l.pos += len(l.currentRightShortcodeDelim())
l.emit(tText)
return lexMainSection
}
func lexShortcodeRightDelim(l *pageLexer) stateFunc {
l.closingState = 0
l.pos += len(l.currentRightShortcodeDelim())
l.emit(l.currentRightShortcodeDelimItem())
return lexMainSection
}
// either:
// 1. param
// 2. "param" or "param\"
// 3. param="123" or param="123\"
// 4. param="Some \"escaped\" text"
// 5. `param`
// 6. param=`123`
func lexShortcodeParam(l *pageLexer, escapedQuoteStart bool) stateFunc {
first := true
nextEq := false
var r rune
for {
r = l.next()
if first {
if r == '"' || (r == '`' && !escapedQuoteStart) {
// a positional param with quotes
if l.paramElements == 2 {
return l.errorf("got quoted positional parameter. Cannot mix named and positional parameters")
}
l.paramElements = 1
l.backup()
if r == '"' {
return lexShortcodeQuotedParamVal(l, !escapedQuoteStart, tScParam)
}
return lexShortCodeParamRawStringVal(l, tScParam)
} else if r == '`' && escapedQuoteStart {
return l.errorf("unrecognized escape character")
}
first = false
} else if r == '=' {
// a named param
l.backup()
nextEq = true
break
}
if !isAlphaNumericOrHyphen(r) && r != '.' { // Floats have period
l.backup()
break
}
}
if l.paramElements == 0 {
l.paramElements++
if nextEq {
l.paramElements++
}
} else {
if nextEq && l.paramElements == 1 {
return l.errorf("got named parameter '%s'. Cannot mix named and positional parameters", l.current())
} else if !nextEq && l.paramElements == 2 {
return l.errorf("got positional parameter '%s'. Cannot mix named and positional parameters", l.current())
}
}
l.emit(tScParam)
return lexInsideShortcode
}
func lexShortcodeParamVal(l *pageLexer) stateFunc {
l.consumeToSpace()
l.emit(tScParamVal)
return lexInsideShortcode
}
func lexShortCodeParamRawStringVal(l *pageLexer, typ ItemType) stateFunc {
openBacktickFound := false
Loop:
for {
switch r := l.next(); {
case r == '`':
if openBacktickFound {
l.backup()
break Loop
} else {
openBacktickFound = true
l.ignore()
}
case r == eof:
return l.errorf("unterminated raw string in shortcode parameter-argument: '%s'", l.current())
}
}
l.emitString(typ)
l.next()
l.ignore()
return lexInsideShortcode
}
func lexShortcodeQuotedParamVal(l *pageLexer, escapedQuotedValuesAllowed bool, typ ItemType) stateFunc {
openQuoteFound := false
escapedInnerQuoteFound := false
escapedQuoteState := 0
Loop:
for {
switch r := l.next(); {
case r == '\\':
if l.peek() == '"' {
if openQuoteFound && !escapedQuotedValuesAllowed {
l.backup()
break Loop
} else if openQuoteFound {
// the coming quote is inside
escapedInnerQuoteFound = true
escapedQuoteState = 1
}
} else if l.peek() == '`' {
return l.errorf("unrecognized escape character")
}
case r == eof, r == '\n':
return l.errorf("unterminated quoted string in shortcode parameter-argument: '%s'", l.current())
case r == '"':
if escapedQuoteState == 0 {
if openQuoteFound {
l.backup()
break Loop
} else {
openQuoteFound = true
l.ignore()
}
} else {
escapedQuoteState = 0
}
}
}
if escapedInnerQuoteFound {
l.ignoreEscapesAndEmit(typ, true)
} else {
l.emitString(typ)
}
r := l.next()
if r == '\\' {
if l.peek() == '"' {
// ignore the escaped closing quote
l.ignore()
l.next()
l.ignore()
}
} else if r == '"' {
// ignore closing quote
l.ignore()
} else {
// handled by next state
l.backup()
}
return lexInsideShortcode
}
// Inline shortcodes has the form {{< myshortcode.inline >}}
var inlineIdentifier = []byte("inline ")
// scans an alphanumeric inside shortcode
func lexIdentifierInShortcode(l *pageLexer) stateFunc {
lookForEnd := false
Loop:
for {
switch r := l.next(); {
case isAlphaNumericOrHyphen(r):
// Allow forward slash inside names to make it possible to create namespaces.
case r == '/':
case r == '.':
l.isInline = l.hasPrefix(inlineIdentifier)
if !l.isInline {
return l.errorf("period in shortcode name only allowed for inline identifiers")
}
default:
l.backup()
word := string(l.input[l.start:l.pos])
if l.closingState > 0 && !l.openShortcodes[word] {
return l.errorf("closing tag for shortcode '%s' does not match start tag", word)
} else if l.closingState > 0 {
l.openShortcodes[word] = false
lookForEnd = true
}
l.closingState = 0
l.currShortcodeName = word
l.openShortcodes[word] = true
l.elementStepNum++
if l.isInline {
l.emit(tScNameInline)
} else {
l.emit(tScName)
}
break Loop
}
}
if lookForEnd {
return lexEndOfShortcode
}
return lexInsideShortcode
}
func lexEndOfShortcode(l *pageLexer) stateFunc {
l.isInline = false
if l.hasPrefix(l.currentRightShortcodeDelim()) {
return lexShortcodeRightDelim
}
switch r := l.next(); {
case isSpace(r):
l.ignore()
default:
return l.errorf("unclosed shortcode")
}
return lexEndOfShortcode
}
// scans the elements inside shortcode tags
func lexInsideShortcode(l *pageLexer) stateFunc {
if l.hasPrefix(l.currentRightShortcodeDelim()) {
return lexShortcodeRightDelim
}
switch r := l.next(); {
case r == eof:
// eol is allowed inside shortcodes; this may go to end of document before it fails
return l.errorf("unclosed shortcode action")
case isSpace(r), isEndOfLine(r):
l.ignore()
case r == '=':
l.consumeSpace()
l.ignore()
peek := l.peek()
if peek == '"' || peek == '\\' {
return lexShortcodeQuotedParamVal(l, peek != '\\', tScParamVal)
} else if peek == '`' {
return lexShortCodeParamRawStringVal(l, tScParamVal)
}
return lexShortcodeParamVal
case r == '/':
if l.currShortcodeName == "" {
return l.errorf("got closing shortcode, but none is open")
}
l.closingState++
l.isInline = false
l.elementStepNum = 0
l.emit(tScClose)
case r == '\\':
l.ignore()
if l.peek() == '"' || l.peek() == '`' {
return lexShortcodeParam(l, true)
}
case l.elementStepNum > 0 && (isAlphaNumericOrHyphen(r) || r == '"' || r == '`'): // positional params can have quotes
l.backup()
return lexShortcodeParam(l, false)
case isAlphaNumeric(r):
l.backup()
return lexIdentifierInShortcode
default:
return l.errorf("unrecognized character in shortcode action: %#U. Note: Parameters with non-alphanumeric args must be quoted", r)
}
return lexInsideShortcode
}
func (l *pageLexer) currentLeftShortcodeDelimItem() ItemType {
return l.currLeftDelimItem
}
func (l *pageLexer) currentRightShortcodeDelimItem() ItemType {
return l.currRightDelimItem
}
func (l *pageLexer) currentLeftShortcodeDelim() []byte {
if l.currLeftDelimItem == tLeftDelimScWithMarkup {
return leftDelimScWithMarkup
}
return leftDelimScNoMarkup
}
func (l *pageLexer) currentRightShortcodeDelim() []byte {
if l.currRightDelimItem == tRightDelimScWithMarkup {
return rightDelimScWithMarkup
}
return rightDelimScNoMarkup
}
// Copyright 2019 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 pageparser
import (
"bytes"
"errors"
"fmt"
"io"
"regexp"
"strings"
"github.com/gohugoio/hugo/parser/metadecoders"
)
// Result holds the parse result.
type Result interface {
// Iterator returns a new Iterator positioned at the beginning of the parse tree.
Iterator() *Iterator
// Input returns the input to Parse.
Input() []byte
}
var _ Result = (*pageLexer)(nil)
// ParseBytes parses the page in b according to the given Config.
func ParseBytes(b []byte, cfg Config) (Items, error) {
startLexer := lexIntroSection
if cfg.NoFrontMatter {
startLexer = lexMainSection
}
l, err := parseBytes(b, cfg, startLexer)
if err != nil {
return nil, err
}
return l.items, l.err
}
type ContentFrontMatter struct {
Content []byte
FrontMatter map[string]any
FrontMatterFormat metadecoders.Format
}
// ParseFrontMatterAndContent is a convenience method to extract front matter
// and content from a content page.
func ParseFrontMatterAndContent(r io.Reader) (ContentFrontMatter, error) {
var cf ContentFrontMatter
input, err := io.ReadAll(r)
if err != nil {
return cf, fmt.Errorf("failed to read page content: %w", err)
}
psr, err := ParseBytes(input, Config{})
if err != nil {
return cf, err
}
var frontMatterSource []byte
iter := NewIterator(psr)
walkFn := func(item Item) bool {
if frontMatterSource != nil {
// The rest is content.
cf.Content = input[item.low:]
// Done
return false
} else if item.IsFrontMatter() {
cf.FrontMatterFormat = FormatFromFrontMatterType(item.Type)
frontMatterSource = item.Val(input)
}
return true
}
iter.PeekWalk(walkFn)
cf.FrontMatter, err = metadecoders.Default.UnmarshalToMap(frontMatterSource, cf.FrontMatterFormat)
return cf, err
}
func FormatFromFrontMatterType(typ ItemType) metadecoders.Format {
switch typ {
case TypeFrontMatterJSON:
return metadecoders.JSON
case TypeFrontMatterORG:
return metadecoders.ORG
case TypeFrontMatterTOML:
return metadecoders.TOML
case TypeFrontMatterYAML:
return metadecoders.YAML
default:
return ""
}
}
// ParseMain parses starting with the main section. Used in tests.
func ParseMain(r io.Reader, cfg Config) (Result, error) {
return parseSection(r, cfg, lexMainSection)
}
func parseSection(r io.Reader, cfg Config, start stateFunc) (Result, error) {
b, err := io.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("failed to read page content: %w", err)
}
return parseBytes(b, cfg, start)
}
func parseBytes(b []byte, cfg Config, start stateFunc) (*pageLexer, error) {
lexer := newPageLexer(b, start, cfg)
lexer.run()
return lexer, nil
}
// NewIterator creates a new Iterator.
func NewIterator(items Items) *Iterator {
return &Iterator{items: items, lastPos: -1}
}
// An Iterator has methods to iterate a parsed page with support going back
// if needed.
type Iterator struct {
items Items
lastPos int // position of the last item returned by nextItem
}
// consumes and returns the next item
func (t *Iterator) Next() Item {
t.lastPos++
return t.Current()
}
var errIndexOutOfBounds = Item{Type: tError, Err: errors.New("no more tokens")}
// Current will repeatably return the current item.
func (t *Iterator) Current() Item {
if t.lastPos >= len(t.items) {
return errIndexOutOfBounds
}
return t.items[t.lastPos]
}
// backs up one token.
func (t *Iterator) Backup() {
if t.lastPos < 0 {
panic("need to go forward before going back")
}
t.lastPos--
}
// Pos returns the current position in the input.
func (t *Iterator) Pos() int {
return t.lastPos
}
// check for non-error and non-EOF types coming next
func (t *Iterator) IsValueNext() bool {
i := t.Peek()
return i.Type != tError && i.Type != tEOF
}
// look at, but do not consume, the next item
// repeated, sequential calls will return the same item
func (t *Iterator) Peek() Item {
return t.items[t.lastPos+1]
}
// PeekWalk will feed the next items in the iterator to walkFn
// until it returns false.
func (t *Iterator) PeekWalk(walkFn func(item Item) bool) {
for i := t.lastPos + 1; i < len(t.items); i++ {
item := t.items[i]
if !walkFn(item) {
break
}
}
}
// Consume is a convenience method to consume the next n tokens,
// but back off Errors and EOF.
func (t *Iterator) Consume(cnt int) {
for range cnt {
token := t.Next()
if token.Type == tError || token.Type == tEOF {
t.Backup()
break
}
}
}
// LineNumber returns the current line number. Used for logging.
func (t *Iterator) LineNumber(source []byte) int {
return bytes.Count(source[:t.Current().low], lf) + 1
}
// IsProbablySourceOfItems returns true if the given source looks like original
// source of the items.
// There may be some false positives, but that is highly unlikely and good enough
// for the planned purpose.
// It will also return false if the last item is not EOF (error situations) and
// true if both source and items are empty.
func IsProbablySourceOfItems(source []byte, items Items) bool {
if len(source) == 0 && len(items) == 0 {
return false
}
if len(items) == 0 {
return false
}
last := items[len(items)-1]
if last.Type != tEOF {
return false
}
if last.Pos() != len(source) {
return false
}
for _, item := range items {
if item.Type == tError {
return false
}
if item.Type == tEOF {
return true
}
if item.Pos() >= len(source) {
return false
}
if item.firstByte != source[item.Pos()] {
return false
}
}
return true
}
var hasShortcodeRe = regexp.MustCompile(`{{[%,<][^\/]`)
// HasShortcode returns true if the given string contains a shortcode.
func HasShortcode(s string) bool {
// Fast path for the common case.
if !strings.Contains(s, "{{") {
return false
}
return hasShortcodeRe.MatchString(s)
}
// Copyright 2024 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 kinds
import (
"sort"
"strings"
)
const (
KindPage = "page"
// The rest are node types; home page, sections etc.
KindHome = "home"
KindSection = "section"
// Note that before Hugo 0.73 these were confusingly named
// taxonomy (now: term)
// taxonomyTerm (now: taxonomy)
KindTaxonomy = "taxonomy"
KindTerm = "term"
// The following are (currently) temporary nodes,
// i.e. nodes we create just to render in isolation.
KindTemporary = "temporary"
KindRSS = "rss"
KindSitemap = "sitemap"
KindSitemapIndex = "sitemapindex"
KindRobotsTXT = "robotstxt"
KindStatus404 = "404"
)
var (
// This is all the kinds we can expect to find in .Site.Pages.
AllKindsInPages []string
// This is all the kinds, including the temporary ones.
AllKinds []string
)
func init() {
for k := range kindMapMain {
AllKindsInPages = append(AllKindsInPages, k)
AllKinds = append(AllKinds, k)
}
for k := range kindMapTemporary {
AllKinds = append(AllKinds, k)
}
// Sort the slices for determinism.
sort.Strings(AllKindsInPages)
sort.Strings(AllKinds)
}
var kindMapMain = map[string]string{
KindPage: KindPage,
KindHome: KindHome,
KindSection: KindSection,
KindTaxonomy: KindTaxonomy,
KindTerm: KindTerm,
// Legacy, pre v0.53.0.
"taxonomyterm": KindTaxonomy,
}
var kindMapTemporary = map[string]string{
KindRSS: KindRSS,
KindSitemap: KindSitemap,
KindRobotsTXT: KindRobotsTXT,
KindStatus404: KindStatus404,
}
// GetKindMain gets the page kind given a string, empty if not found.
// Note that this will not return any temporary kinds (e.g. robotstxt).
func GetKindMain(s string) string {
return kindMapMain[strings.ToLower(s)]
}
// GetKindAny gets the page kind given a string, empty if not found.
func GetKindAny(s string) string {
if pkind := GetKindMain(s); pkind != "" {
return pkind
}
return kindMapTemporary[strings.ToLower(s)]
}
// IsBranch returns whether the given kind is a branch node.
func IsBranch(kind string) bool {
switch kind {
case KindHome, KindSection, KindTaxonomy, KindTerm:
return true
default:
return false
}
}
// IsDeprecatedAndReplacedWith returns the new kind if the given kind is deprecated.
func IsDeprecatedAndReplacedWith(s string) string {
s = strings.ToLower(s)
switch s {
case "taxonomyterm":
return KindTaxonomy
default:
return ""
}
}