diff options
Diffstat (limited to 'common')
-rw-r--r-- | common/hstrings/strings.go | 57 | ||||
-rw-r--r-- | common/hstrings/strings_test.go | 36 | ||||
-rw-r--r-- | common/htime/time.go | 9 | ||||
-rw-r--r-- | common/hugo/hugo.go | 27 | ||||
-rw-r--r-- | common/loggers/ignorableLogger.go | 10 | ||||
-rw-r--r-- | common/maps/maps.go | 35 | ||||
-rw-r--r-- | common/maps/maps_test.go | 8 | ||||
-rw-r--r-- | common/maps/params.go | 98 | ||||
-rw-r--r-- | common/maps/params_test.go | 16 | ||||
-rw-r--r-- | common/urls/baseURL.go | 110 | ||||
-rw-r--r-- | common/urls/baseURL_test.go | 67 |
11 files changed, 413 insertions, 60 deletions
diff --git a/common/hstrings/strings.go b/common/hstrings/strings.go new file mode 100644 index 000000000..6c0f820fe --- /dev/null +++ b/common/hstrings/strings.go @@ -0,0 +1,57 @@ +// Copyright 2023 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 hstrings + +import ( + "fmt" + "strings" + + "github.com/gohugoio/hugo/compare" +) + +var _ compare.Eqer = StringEqualFold("") + +// StringEqualFold is a string that implements the compare.Eqer interface and considers +// two strings equal if they are equal when folded to lower case. +// The compare.Eqer interface is used in Hugo to compare values in templates (e.g. using the eq template function). +type StringEqualFold string + +func (s StringEqualFold) EqualFold(s2 string) bool { + return strings.EqualFold(string(s), s2) +} + +func (s StringEqualFold) String() string { + return string(s) +} + +func (s StringEqualFold) Eq(s2 any) bool { + switch ss := s2.(type) { + case string: + return s.EqualFold(ss) + case fmt.Stringer: + return s.EqualFold(ss.String()) + } + + return false +} + +// EqualAny returns whether a string is equal to any of the given strings. +func EqualAny(a string, b ...string) bool { + for _, s := range b { + if a == s { + return true + } + } + return false +} diff --git a/common/hstrings/strings_test.go b/common/hstrings/strings_test.go new file mode 100644 index 000000000..dc2eae6f2 --- /dev/null +++ b/common/hstrings/strings_test.go @@ -0,0 +1,36 @@ +// Copyright 2023 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 hstrings + +import ( + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestStringEqualFold(t *testing.T) { + c := qt.New(t) + + s1 := "A" + s2 := "a" + + c.Assert(StringEqualFold(s1).EqualFold(s2), qt.Equals, true) + c.Assert(StringEqualFold(s1).EqualFold(s1), qt.Equals, true) + c.Assert(StringEqualFold(s2).EqualFold(s1), qt.Equals, true) + c.Assert(StringEqualFold(s2).EqualFold(s2), qt.Equals, true) + c.Assert(StringEqualFold(s1).EqualFold("b"), qt.Equals, false) + c.Assert(StringEqualFold(s1).Eq(s2), qt.Equals, true) + c.Assert(StringEqualFold(s1).Eq("b"), qt.Equals, false) + +} diff --git a/common/htime/time.go b/common/htime/time.go index d30ecf7e1..961962b60 100644 --- a/common/htime/time.go +++ b/common/htime/time.go @@ -14,6 +14,7 @@ package htime import ( + "log" "strings" "time" @@ -163,3 +164,11 @@ func Since(t time.Time) time.Duration { type AsTimeProvider interface { AsTime(zone *time.Location) time.Time } + +// StopWatch is a simple helper to measure time during development. +func StopWatch(name string) func() { + start := time.Now() + return func() { + log.Printf("StopWatch %q took %s", name, time.Since(start)) + } +} diff --git a/common/hugo/hugo.go b/common/hugo/hugo.go index efcb470a3..6402d7b88 100644 --- a/common/hugo/hugo.go +++ b/common/hugo/hugo.go @@ -46,8 +46,8 @@ var ( vendorInfo string ) -// Info contains information about the current Hugo environment -type Info struct { +// HugoInfo contains information about the current Hugo environment +type HugoInfo struct { CommitHash string BuildDate string @@ -64,30 +64,30 @@ type Info struct { } // Version returns the current version as a comparable version string. -func (i Info) Version() VersionString { +func (i HugoInfo) Version() VersionString { return CurrentVersion.Version() } // Generator a Hugo meta generator HTML tag. -func (i Info) Generator() template.HTML { +func (i HugoInfo) Generator() template.HTML { return template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s">`, CurrentVersion.String())) } -func (i Info) IsProduction() bool { +func (i HugoInfo) IsProduction() bool { return i.Environment == EnvironmentProduction } -func (i Info) IsExtended() bool { +func (i HugoInfo) IsExtended() bool { return IsExtended } // Deps gets a list of dependencies for this Hugo build. -func (i Info) Deps() []*Dependency { +func (i HugoInfo) Deps() []*Dependency { return i.deps } // NewInfo creates a new Hugo Info object. -func NewInfo(environment string, deps []*Dependency) Info { +func NewInfo(environment string, deps []*Dependency) HugoInfo { if environment == "" { environment = EnvironmentProduction } @@ -104,7 +104,7 @@ func NewInfo(environment string, deps []*Dependency) Info { goVersion = bi.GoVersion } - return Info{ + return HugoInfo{ CommitHash: commitHash, BuildDate: buildDate, Environment: environment, @@ -115,7 +115,7 @@ func NewInfo(environment string, deps []*Dependency) Info { // GetExecEnviron creates and gets the common os/exec environment used in the // external programs we interact with via os/exec, e.g. postcss. -func GetExecEnviron(workDir string, cfg config.Provider, fs afero.Fs) []string { +func GetExecEnviron(workDir string, cfg config.AllProvider, fs afero.Fs) []string { var env []string nodepath := filepath.Join(workDir, "node_modules") if np := os.Getenv("NODE_PATH"); np != "" { @@ -123,10 +123,9 @@ func GetExecEnviron(workDir string, cfg config.Provider, fs afero.Fs) []string { } config.SetEnvVars(&env, "NODE_PATH", nodepath) config.SetEnvVars(&env, "PWD", workDir) - config.SetEnvVars(&env, "HUGO_ENVIRONMENT", cfg.GetString("environment")) - config.SetEnvVars(&env, "HUGO_ENV", cfg.GetString("environment")) - - config.SetEnvVars(&env, "HUGO_PUBLISHDIR", filepath.Join(workDir, cfg.GetString("publishDirOrig"))) + config.SetEnvVars(&env, "HUGO_ENVIRONMENT", cfg.Environment()) + config.SetEnvVars(&env, "HUGO_ENV", cfg.Environment()) + config.SetEnvVars(&env, "HUGO_PUBLISHDIR", filepath.Join(workDir, cfg.BaseConfig().PublishDir)) if fs != nil { fis, err := afero.ReadDir(fs, files.FolderJSConfig) diff --git a/common/loggers/ignorableLogger.go b/common/loggers/ignorableLogger.go index 5040d1036..c8aba560e 100644 --- a/common/loggers/ignorableLogger.go +++ b/common/loggers/ignorableLogger.go @@ -15,7 +15,6 @@ package loggers import ( "fmt" - "strings" ) // IgnorableLogger is a logger that ignores certain log statements. @@ -31,14 +30,13 @@ type ignorableLogger struct { } // NewIgnorableLogger wraps the given logger and ignores the log statement IDs given. -func NewIgnorableLogger(logger Logger, statements ...string) IgnorableLogger { - statementsSet := make(map[string]bool) - for _, s := range statements { - statementsSet[strings.ToLower(s)] = true +func NewIgnorableLogger(logger Logger, statements map[string]bool) IgnorableLogger { + if statements == nil { + statements = make(map[string]bool) } return ignorableLogger{ Logger: logger, - statements: statementsSet, + statements: statements, } } diff --git a/common/maps/maps.go b/common/maps/maps.go index 2d8a122ca..6aefde927 100644 --- a/common/maps/maps.go +++ b/common/maps/maps.go @@ -43,25 +43,25 @@ func ToStringMapE(in any) (map[string]any, error) { // 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, bool) { +func ToParamsAndPrepare(in any) (Params, error) { if types.IsNil(in) { - return Params{}, true + return Params{}, nil } m, err := ToStringMapE(in) if err != nil { - return nil, false + return nil, err } PrepareParams(m) - return m, true + return m, nil } // MustToParamsAndPrepare calls ToParamsAndPrepare and panics if it fails. func MustToParamsAndPrepare(in any) Params { - if p, ok := ToParamsAndPrepare(in); ok { - return p - } else { - panic(fmt.Sprintf("cannot convert %T to maps.Params", in)) + 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{}. @@ -96,6 +96,8 @@ 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 { @@ -123,6 +125,23 @@ func LookupEqualFold[T any | string](m map[string]T, key string) (T, bool) { 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 diff --git a/common/maps/maps_test.go b/common/maps/maps_test.go index 0b84d2dd7..0e8589d34 100644 --- a/common/maps/maps_test.go +++ b/common/maps/maps_test.go @@ -116,11 +116,11 @@ func TestToSliceStringMap(t *testing.T) { func TestToParamsAndPrepare(t *testing.T) { c := qt.New(t) - _, ok := ToParamsAndPrepare(map[string]any{"A": "av"}) - c.Assert(ok, qt.IsTrue) + _, err := ToParamsAndPrepare(map[string]any{"A": "av"}) + c.Assert(err, qt.IsNil) - params, ok := ToParamsAndPrepare(nil) - c.Assert(ok, qt.IsTrue) + params, err := ToParamsAndPrepare(nil) + c.Assert(err, qt.IsNil) c.Assert(params, qt.DeepEquals, Params{}) } diff --git a/common/maps/params.go b/common/maps/params.go index 4bf95f43b..eb60fbbfc 100644 --- a/common/maps/params.go +++ b/common/maps/params.go @@ -23,30 +23,37 @@ import ( // Params is a map where all keys are lower case. type Params map[string]any -// Get does a lower case and nested search in this map. +// 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. -func (p Params) Get(indices ...string) any { +// Make all of these methods internal somehow. +func (p Params) GetNested(indices ...string) any { v, _, _ := getNested(p, indices) return v } -// Set overwrites values in p with values in pp for common or new keys. +// Set overwrites values in dst with values in src for common or new keys. // This is done recursively. -func (p Params) Set(pp Params) { - for k, v := range pp { - vv, found := p[k] +func SetParams(dst, src Params) { + for k, v := range src { + vv, found := dst[k] if !found { - p[k] = v + dst[k] = v } else { switch vvv := vv.(type) { case Params: if pv, ok := v.(Params); ok { - vvv.Set(pv) + SetParams(vvv, pv) } else { - p[k] = v + dst[k] = v } default: - p[k] = v + dst[k] = v } } } @@ -70,18 +77,17 @@ func (p Params) IsZero() bool { } -// Merge transfers values from pp to p for new keys. +// MergeParamsWithStrategy transfers values from src to dst for new keys using the merge strategy given. // This is done recursively. -func (p Params) Merge(pp Params) { - p.merge("", pp) +func MergeParamsWithStrategy(strategy string, dst, src Params) { + dst.merge(ParamsMergeStrategy(strategy), src) } -// MergeRoot transfers values from pp to p for new keys where p is the -// root of the tree. +// MergeParamsWithStrategy transfers values from src to dst for new keys using the merge encoded in dst. // This is done recursively. -func (p Params) MergeRoot(pp Params) { - ms, _ := p.GetMergeStrategy() - p.merge(ms, pp) +func MergeParams(dst, src Params) { + ms, _ := dst.GetMergeStrategy() + dst.merge(ms, src) } func (p Params) merge(ps ParamsMergeStrategy, pp Params) { @@ -116,6 +122,7 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) { } } +// For internal use. func (p Params) GetMergeStrategy() (ParamsMergeStrategy, bool) { if v, found := p[mergeStrategyKey]; found { if s, ok := v.(ParamsMergeStrategy); ok { @@ -125,6 +132,7 @@ func (p Params) GetMergeStrategy() (ParamsMergeStrategy, bool) { return ParamsMergeStrategyShallow, false } +// For internal use. func (p Params) DeleteMergeStrategy() bool { if _, found := p[mergeStrategyKey]; found { delete(p, mergeStrategyKey) @@ -133,7 +141,8 @@ func (p Params) DeleteMergeStrategy() bool { return false } -func (p Params) SetDefaultMergeStrategy(s ParamsMergeStrategy) { +// For internal use. +func (p Params) SetMergeStrategy(s ParamsMergeStrategy) { switch s { case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow: default: @@ -187,7 +196,7 @@ func GetNestedParam(keyStr, separator string, candidates ...Params) (any, error) keySegments := strings.Split(keyStr, separator) for _, m := range candidates { - if v := m.Get(keySegments...); v != nil { + if v := m.GetNested(keySegments...); v != nil { return v, nil } } @@ -236,6 +245,55 @@ const ( mergeStrategyKey = "_merge" ) +// CleanConfigStringMapString removes any processing instructions from m, +// m will never be modified. +func CleanConfigStringMapString(m map[string]string) map[string]string { + if m == nil || 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 m == nil || 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 { diff --git a/common/maps/params_test.go b/common/maps/params_test.go index a070e6f60..7e1dbbae7 100644 --- a/common/maps/params_test.go +++ b/common/maps/params_test.go @@ -81,7 +81,7 @@ func TestParamsSetAndMerge(t *testing.T) { p1, p2 := createParamsPair() - p1.Set(p2) + SetParams(p1, p2) c.Assert(p1, qt.DeepEquals, Params{ "a": "abv", @@ -97,7 +97,7 @@ func TestParamsSetAndMerge(t *testing.T) { p1, p2 = createParamsPair() - p1.Merge(p2) + MergeParamsWithStrategy("", p1, p2) // Default is to do a shallow merge. c.Assert(p1, qt.DeepEquals, Params{ @@ -111,8 +111,8 @@ func TestParamsSetAndMerge(t *testing.T) { }) p1, p2 = createParamsPair() - p1.SetDefaultMergeStrategy(ParamsMergeStrategyNone) - p1.Merge(p2) + p1.SetMergeStrategy(ParamsMergeStrategyNone) + MergeParamsWithStrategy("", p1, p2) p1.DeleteMergeStrategy() c.Assert(p1, qt.DeepEquals, Params{ @@ -125,8 +125,8 @@ func TestParamsSetAndMerge(t *testing.T) { }) p1, p2 = createParamsPair() - p1.SetDefaultMergeStrategy(ParamsMergeStrategyShallow) - p1.Merge(p2) + p1.SetMergeStrategy(ParamsMergeStrategyShallow) + MergeParamsWithStrategy("", p1, p2) p1.DeleteMergeStrategy() c.Assert(p1, qt.DeepEquals, Params{ @@ -140,8 +140,8 @@ func TestParamsSetAndMerge(t *testing.T) { }) p1, p2 = createParamsPair() - p1.SetDefaultMergeStrategy(ParamsMergeStrategyDeep) - p1.Merge(p2) + p1.SetMergeStrategy(ParamsMergeStrategyDeep) + MergeParamsWithStrategy("", p1, p2) p1.DeleteMergeStrategy() c.Assert(p1, qt.DeepEquals, Params{ diff --git a/common/urls/baseURL.go b/common/urls/baseURL.go new file mode 100644 index 000000000..df26730ec --- /dev/null +++ b/common/urls/baseURL.go @@ -0,0 +1,110 @@ +// Copyright 2023 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 urls + +import ( + "fmt" + "net/url" + "strconv" + "strings" +) + +// A BaseURL in Hugo is normally on the form scheme://path, but the +// form scheme: is also valid (mailto:hugo@rules.com). +type BaseURL struct { + url *url.URL + WithPath string + WithoutPath string + BasePath string +} + +func (b BaseURL) String() string { + return b.WithPath +} + +func (b BaseURL) Path() string { + return b.url.Path +} + +func (b BaseURL) Port() int { + p, _ := strconv.Atoi(b.url.Port()) + return p +} + +// HostURL returns the URL to the host root without any path elements. +func (b BaseURL) HostURL() string { + return strings.TrimSuffix(b.String(), b.Path()) +} + +// WithProtocol returns the BaseURL prefixed with the given protocol. +// The Protocol is normally of the form "scheme://", i.e. "webcal://". +func (b BaseURL) WithProtocol(protocol string) (BaseURL, error) { + u := b.URL() + + scheme := protocol + isFullProtocol := strings.HasSuffix(scheme, "://") + isOpaqueProtocol := strings.HasSuffix(scheme, ":") + + if isFullProtocol { + scheme = strings.TrimSuffix(scheme, "://") + } else if isOpaqueProtocol { + scheme = strings.TrimSuffix(scheme, ":") + } + + u.Scheme = scheme + + if isFullProtocol && u.Opaque != "" { + u.Opaque = "//" + u.Opaque + } else if isOpaqueProtocol && u.Opaque == "" { + return BaseURL{}, fmt.Errorf("cannot determine BaseURL for protocol %q", protocol) + } + + return newBaseURLFromURL(u) +} + +func (b BaseURL) WithPort(port int) (BaseURL, error) { + u := b.URL() + u.Host = u.Hostname() + ":" + strconv.Itoa(port) + return newBaseURLFromURL(u) +} + +// URL returns a copy of the internal URL. +// The copy can be safely used and modified. +func (b BaseURL) URL() *url.URL { + c := *b.url + return &c +} + +func NewBaseURLFromString(b string) (BaseURL, error) { + u, err := url.Parse(b) + if err != nil { + return BaseURL{}, err + } + return newBaseURLFromURL(u) + +} + +func newBaseURLFromURL(u *url.URL) (BaseURL, error) { + baseURL := BaseURL{url: u, WithPath: u.String()} + var baseURLNoPath = baseURL.URL() + baseURLNoPath.Path = "" + baseURL.WithoutPath = baseURLNoPath.String() + + basePath := u.Path + if basePath != "" && basePath != "/" { + baseURL.BasePath = basePath + } + + return baseURL, nil +} diff --git a/common/urls/baseURL_test.go b/common/urls/baseURL_test.go new file mode 100644 index 000000000..95dc73339 --- /dev/null +++ b/common/urls/baseURL_test.go @@ -0,0 +1,67 @@ +// Copyright 2023 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 urls + +import ( + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestBaseURL(t *testing.T) { + c := qt.New(t) + b, err := NewBaseURLFromString("http://example.com") + c.Assert(err, qt.IsNil) + c.Assert(b.String(), qt.Equals, "http://example.com") + + p, err := b.WithProtocol("webcal://") + c.Assert(err, qt.IsNil) + c.Assert(p.String(), qt.Equals, "webcal://example.com") + + p, err = b.WithProtocol("webcal") + c.Assert(err, qt.IsNil) + c.Assert(p.String(), qt.Equals, "webcal://example.com") + + _, err = b.WithProtocol("mailto:") + c.Assert(err, qt.Not(qt.IsNil)) + + b, err = NewBaseURLFromString("mailto:hugo@rules.com") + c.Assert(err, qt.IsNil) + c.Assert(b.String(), qt.Equals, "mailto:hugo@rules.com") + + // These are pretty constructed + p, err = b.WithProtocol("webcal") + c.Assert(err, qt.IsNil) + c.Assert(p.String(), qt.Equals, "webcal:hugo@rules.com") + + p, err = b.WithProtocol("webcal://") + c.Assert(err, qt.IsNil) + c.Assert(p.String(), qt.Equals, "webcal://hugo@rules.com") + + // Test with "non-URLs". Some people will try to use these as a way to get + // relative URLs working etc. + b, err = NewBaseURLFromString("/") + c.Assert(err, qt.IsNil) + c.Assert(b.String(), qt.Equals, "/") + + b, err = NewBaseURLFromString("") + c.Assert(err, qt.IsNil) + c.Assert(b.String(), qt.Equals, "") + + // BaseURL with sub path + b, err = NewBaseURLFromString("http://example.com/sub") + c.Assert(err, qt.IsNil) + c.Assert(b.String(), qt.Equals, "http://example.com/sub") + c.Assert(b.HostURL(), qt.Equals, "http://example.com") +} |