diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go new file mode 100644 index 00000000000..c642494d086 --- /dev/null +++ b/config/allconfig/allconfig.go @@ -0,0 +1,170 @@ +// 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 allconfig contains the full configuration for Hugo. +package allconfig + +import ( + "github.com/gohugoio/hugo/cache/filecache" + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/config/privacy" + "github.com/gohugoio/hugo/config/security" + "github.com/gohugoio/hugo/config/services" + "github.com/gohugoio/hugo/markup/markup_config" + "github.com/gohugoio/hugo/minifiers" + "github.com/gohugoio/hugo/modules" + "github.com/gohugoio/hugo/related" + "github.com/gohugoio/hugo/resources/images" +) + +/* +ConfigRootKeysSet = map[string]bool{ + "build": true, + "caches": true, + "cascade": true, + "frontmatter": true, + "languages": true, + "imaging": true, + "markup": true, + "mediatypes": true, + "menus": true, + "minify": true, + "module": true, + "outputformats": true, + "params": true, + "permalinks": true, + "related": true, + + "sitemap": true, + "privacy": true, + "security": true, + "taxonomies": true, + } +*/ + +// Config holds all configuration options in Hugo. +// TODO(bep) make sure that the keys gets the sensible casing in JSON etc. +type Config struct { + // The build configuration section contains global build-related configuration options. + Build config.Build + + // The caches configuration section contains global cache-related configuration options. + Caches map[string]filecache.Config + + // The cascade configuration section contains global the top level front matter cascade configuration options. + Cascade []CascadeEntryConfig + + // Front matter configuration. + Frontmatter FrontmatterConfig + + // Image processing configuration. + Imaging images.ImagingConfig + + // Language configuration. + Languages map[string]LanguageConfig + + // Markup configuration. + Markup markup_config.Config + + // Media type configuration. + MediaTypes map[string]MediaTypeConfig + + // Menu configuration. + Menus []MenuConfig + + // Minification configuration. + Minify minifiers.MinifyConfig + + // Module configuration. + Module modules.Config + + // Output format configuration. + Outputformats map[string]OutputFormatConfig + + // Params configuration. + Params maps.Params + + // Permalink configuration. + Permalinks map[string]string + + // Privacy configuration. + Privacy privacy.Config + + // Related content configuration. + Related related.Config + + // Security configuration. + Security security.Config + + // Services configuration. + Services services.Config + + // Sitemap configuration. + Sitemap config.Sitemap + + // Taxonomy configuration. + Taxonomies map[string]string + + // TODO(bep) root options. +} + +// TODO(bep) move these. +type CascadeEntryConfig struct { + // A Glob pattern matching the content path below /content. + // Expects Unix-styled slashes. Note that this is the virtual path, so it starts at the mount root. + // The matching supports double-asterisks so you can match for patterns like /blog/*/** to match anything from the third level and + Path string + + // A Glob pattern matching the Page’s Kind(s), e.g. “{home,section}”. + Kind string + + // A Glob pattern matching the Page’s language, e.g. “{en,sv}”. + Lang string + + // A Glob pattern matching the build environment, e.g. “{production,development}” + Environment string +} + +type OutputFormatConfig struct { + BaseName string + IsPlainText bool + MediaType string + Protocol string +} + +type FrontmatterConfig struct { + Date []string + ExpiryDate []string + LastMod []string + PublishDate []string +} + +type LanguageConfig struct { + LanguageDirection string + Title string + Weight int + Params maps.Params +} + +type MediaTypeConfig struct { + Suffixes []string +} + +type MenuConfig struct { + Identifier string + Name string + Pre string + URL string + Weight int +} diff --git a/foo/foo.go b/foo/foo.go new file mode 100644 index 00000000000..e7524405076 --- /dev/null +++ b/foo/foo.go @@ -0,0 +1,5 @@ +package foo + +func Foo() []string { + return []string{"foo", "bar"} +} diff --git a/hugolib/site.go b/hugolib/site.go index 2ffc3a346f3..6329216031c 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -33,7 +33,9 @@ import ( "github.com/gohugoio/hugo/common/htime" "github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/common/types" + "github.com/gohugoio/hugo/media/mediaconfig" "github.com/gohugoio/hugo/modules" + "github.com/gohugoio/hugo/output/outputconfig" "golang.org/x/text/unicode/norm" "github.com/gohugoio/hugo/common/paths" @@ -450,12 +452,12 @@ But this also means that your site configuration may not do what you expect. If } } - siteMediaTypesConfig, err = media.DecodeTypes(mediaTypesConfig...) + siteMediaTypesConfig, err = mediaconfig.DecodeTypes(mediaTypesConfig...) if err != nil { return nil, err } - siteOutputFormatsConfig, err = output.DecodeFormats(siteMediaTypesConfig, outputFormatsConfig...) + siteOutputFormatsConfig, err = outputconfig.DecodeFormats(siteMediaTypesConfig, outputFormatsConfig...) if err != nil { return nil, err } diff --git a/media/mediaType.go b/media/mediaType.go index cdfb1c6542b..458275693f2 100644 --- a/media/mediaType.go +++ b/media/mediaType.go @@ -16,23 +16,16 @@ package media import ( "encoding/json" - "errors" "fmt" "net/http" "sort" "strings" - - "github.com/spf13/cast" - - "github.com/gohugoio/hugo/common/maps" - - "github.com/mitchellh/mapstructure" ) var zero Type const ( - defaultDelimiter = "." + DefaultDelimiter = "." ) // Type (also known as MIME type and content type) is a two-part identifier for @@ -55,7 +48,7 @@ type Type struct { // E.g. "jpg,jpeg" // Stored as a string to make Type comparable. - suffixesCSV string + SuffixesCSV string `json:"-"` } // SuffixInfo holds information about a Type's suffix. @@ -117,19 +110,19 @@ func FromContent(types Types, extensionHints []string, content []byte) Type { // FromStringAndExt creates a Type from a MIME string and a given extension. func FromStringAndExt(t, ext string) (Type, error) { - tp, err := fromString(t) + tp, err := FromString(t) if err != nil { return tp, err } - tp.suffixesCSV = strings.TrimPrefix(ext, ".") - tp.Delimiter = defaultDelimiter + tp.SuffixesCSV = strings.TrimPrefix(ext, ".") + tp.Delimiter = DefaultDelimiter tp.init() return tp, nil } // FromString creates a new Type given a type string on the form MainType/SubType and // an optional suffix, e.g. "text/html" or "text/html+html". -func fromString(t string) (Type, error) { +func FromString(t string) (Type, error) { t = strings.ToLower(t) parts := strings.Split(t, "/") if len(parts) != 2 { @@ -171,11 +164,11 @@ func (m Type) String() string { // Suffixes returns all valid file suffixes for this type. func (m Type) Suffixes() []string { - if m.suffixesCSV == "" { + if m.SuffixesCSV == "" { return nil } - return strings.Split(m.suffixesCSV, ",") + return strings.Split(m.SuffixesCSV, ",") } // IsText returns whether this Type is a text format. @@ -192,6 +185,10 @@ func (m Type) IsText() bool { return false } +func InitMediaType(m *Type) { + m.init() +} + func (m *Type) init() { m.FirstSuffix.FullSuffix = "" m.FirstSuffix.Suffix = "" @@ -204,13 +201,13 @@ func (m *Type) init() { // WithDelimiterAndSuffixes is used in tests. func WithDelimiterAndSuffixes(t Type, delimiter, suffixesCSV string) Type { t.Delimiter = delimiter - t.suffixesCSV = suffixesCSV + t.SuffixesCSV = suffixesCSV t.init() return t } func newMediaType(main, sub string, suffixes []string) Type { - t := Type{MainType: main, SubType: sub, suffixesCSV: strings.Join(suffixes, ","), Delimiter: defaultDelimiter} + t := Type{MainType: main, SubType: sub, SuffixesCSV: strings.Join(suffixes, ","), Delimiter: DefaultDelimiter} t.init() return t } @@ -400,7 +397,7 @@ func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) { } func (m Type) hasSuffix(suffix string) bool { - return strings.Contains(","+m.suffixesCSV+",", ","+suffix+",") + return strings.Contains(","+m.SuffixesCSV+",", ","+suffix+",") } // GetByMainSubType gets a media type given a main and a sub type e.g. "text" and "plain". @@ -423,96 +420,6 @@ func (t Types) GetByMainSubType(mainType, subType string) (tp Type, found bool) return } -func suffixIsRemoved() error { - return errors.New(`MediaType.Suffix is removed. Before Hugo 0.44 this was used both to set a custom file suffix and as way -to augment the mediatype definition (what you see after the "+", e.g. "image/svg+xml"). - -This had its limitations. For one, it was only possible with one file extension per MIME type. - -Now you can specify multiple file suffixes using "suffixes", but you need to specify the full MIME type -identifier: - -[mediaTypes] -[mediaTypes."image/svg+xml"] -suffixes = ["svg", "abc" ] - -In most cases, it will be enough to just change: - -[mediaTypes] -[mediaTypes."my/custom-mediatype"] -suffix = "txt" - -To: - -[mediaTypes] -[mediaTypes."my/custom-mediatype"] -suffixes = ["txt"] - -Note that you can still get the Media Type's suffix from a template: {{ $mediaType.Suffix }}. But this will now map to the MIME type filename. -`) -} - -// DecodeTypes takes a list of media type configurations and merges those, -// in the order given, with the Hugo defaults as the last resort. -func DecodeTypes(mms ...map[string]any) (Types, error) { - var m Types - - // Maps type string to Type. Type string is the full application/svg+xml. - mmm := make(map[string]Type) - for _, dt := range DefaultTypes { - mmm[dt.Type()] = dt - } - - for _, mm := range mms { - for k, v := range mm { - var mediaType Type - - mediaType, found := mmm[k] - if !found { - var err error - mediaType, err = fromString(k) - if err != nil { - return m, err - } - } - - if err := mapstructure.WeakDecode(v, &mediaType); err != nil { - return m, err - } - - vm := maps.ToStringMap(v) - maps.PrepareParams(vm) - _, delimiterSet := vm["delimiter"] - _, suffixSet := vm["suffix"] - - if suffixSet { - return Types{}, suffixIsRemoved() - } - - if suffixes, found := vm["suffixes"]; found { - mediaType.suffixesCSV = strings.TrimSpace(strings.ToLower(strings.Join(cast.ToStringSlice(suffixes), ","))) - } - - // The user may set the delimiter as an empty string. - if !delimiterSet && mediaType.suffixesCSV != "" { - mediaType.Delimiter = defaultDelimiter - } - - mediaType.init() - - mmm[k] = mediaType - - } - } - - for _, v := range mmm { - m = append(m, v) - } - sort.Sort(m) - - return m, nil -} - // IsZero reports whether this Type represents a zero value. // For internal use. func (m Type) IsZero() bool { @@ -532,6 +439,6 @@ func (m Type) MarshalJSON() ([]byte, error) { Alias: (Alias)(m), Type: m.Type(), String: m.String(), - Suffixes: strings.Split(m.suffixesCSV, ","), + Suffixes: strings.Split(m.SuffixesCSV, ","), }) } diff --git a/media/mediaType_test.go b/media/mediaType_test.go index 2a1b4884999..3d12c31bb69 100644 --- a/media/mediaType_test.go +++ b/media/mediaType_test.go @@ -132,22 +132,22 @@ func TestGetFirstBySuffix(t *testing.T) { func TestFromTypeString(t *testing.T) { c := qt.New(t) - f, err := fromString("text/html") + f, err := FromString("text/html") c.Assert(err, qt.IsNil) c.Assert(f.Type(), qt.Equals, HTMLType.Type()) - f, err = fromString("application/custom") + f, err = FromString("application/custom") c.Assert(err, qt.IsNil) c.Assert(f, qt.Equals, Type{MainType: "application", SubType: "custom", mimeSuffix: ""}) - f, err = fromString("application/custom+sfx") + f, err = FromString("application/custom+sfx") c.Assert(err, qt.IsNil) c.Assert(f, qt.Equals, Type{MainType: "application", SubType: "custom", mimeSuffix: "sfx"}) - _, err = fromString("noslash") + _, err = FromString("noslash") c.Assert(err, qt.Not(qt.IsNil)) - f, err = fromString("text/xml; charset=utf-8") + f, err = FromString("text/xml; charset=utf-8") c.Assert(err, qt.IsNil) c.Assert(f, qt.Equals, Type{MainType: "text", SubType: "xml", mimeSuffix: ""}) diff --git a/media/mediaconfig/config.go b/media/mediaconfig/config.go new file mode 100644 index 00000000000..703ec73609e --- /dev/null +++ b/media/mediaconfig/config.go @@ -0,0 +1,115 @@ +// 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 mediaconfig + +import ( + "errors" + "sort" + "strings" + + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/media" + "github.com/mitchellh/mapstructure" + "github.com/spf13/cast" +) + +// DecodeTypes takes a list of media type configurations and merges those, +// in the order given, with the Hugo defaults as the last resort. +func DecodeTypes(mms ...map[string]any) (media.Types, error) { + var m media.Types + + // Maps type string to Type. Type string is the full application/svg+xml. + mmm := make(map[string]media.Type) + for _, dt := range media.DefaultTypes { + mmm[dt.Type()] = dt + } + + for _, mm := range mms { + for k, v := range mm { + var mediaType media.Type + + mediaType, found := mmm[k] + if !found { + var err error + mediaType, err = media.FromString(k) + if err != nil { + return m, err + } + } + + if err := mapstructure.WeakDecode(v, &mediaType); err != nil { + return m, err + } + + vm := maps.ToStringMap(v) + maps.PrepareParams(vm) + _, delimiterSet := vm["delimiter"] + _, suffixSet := vm["suffix"] + + if suffixSet { + return media.Types{}, suffixIsRemoved() + } + + if suffixes, found := vm["suffixes"]; found { + mediaType.SuffixesCSV = strings.TrimSpace(strings.ToLower(strings.Join(cast.ToStringSlice(suffixes), ","))) + } + + // The user may set the delimiter as an empty string. + if !delimiterSet && mediaType.SuffixesCSV != "" { + mediaType.Delimiter = media.DefaultDelimiter + } + + media.InitMediaType(&mediaType) + + mmm[k] = mediaType + + } + } + + for _, v := range mmm { + m = append(m, v) + } + sort.Sort(m) + + return m, nil +} + +func suffixIsRemoved() error { + return errors.New(`MediaType.Suffix is removed. Before Hugo 0.44 this was used both to set a custom file suffix and as way +to augment the mediatype definition (what you see after the "+", e.g. "image/svg+xml"). + +This had its limitations. For one, it was only possible with one file extension per MIME type. + +Now you can specify multiple file suffixes using "suffixes", but you need to specify the full MIME type +identifier: + +[mediaTypes] +[mediaTypes."image/svg+xml"] +suffixes = ["svg", "abc" ] + +In most cases, it will be enough to just change: + +[mediaTypes] +[mediaTypes."my/custom-mediatype"] +suffix = "txt" + +To: + +[mediaTypes] +[mediaTypes."my/custom-mediatype"] +suffixes = ["txt"] + +Note that you can still get the Media Type's suffix from a template: {{ $mediaType.Suffix }}. But this will now map to the MIME type filename. +`) +} diff --git a/minifiers/config.go b/minifiers/config.go index 233f53c2717..0acde0126f8 100644 --- a/minifiers/config.go +++ b/minifiers/config.go @@ -29,7 +29,7 @@ import ( "github.com/tdewolff/minify/v2/xml" ) -var defaultTdewolffConfig = tdewolffConfig{ +var defaultTdewolffConfig = TdewolffConfig{ HTML: html.Minifier{ KeepDocumentTags: true, KeepConditionalComments: true, @@ -52,7 +52,7 @@ var defaultTdewolffConfig = tdewolffConfig{ }, } -type tdewolffConfig struct { +type TdewolffConfig struct { HTML html.Minifier CSS css.Minifier JS js.Minifier @@ -61,7 +61,7 @@ type tdewolffConfig struct { XML xml.Minifier } -type minifyConfig struct { +type MinifyConfig struct { // Whether to minify the published output (the HTML written to /public). MinifyOutput bool @@ -72,14 +72,14 @@ type minifyConfig struct { DisableSVG bool DisableXML bool - Tdewolff tdewolffConfig + Tdewolff TdewolffConfig } -var defaultConfig = minifyConfig{ +var defaultConfig = MinifyConfig{ Tdewolff: defaultTdewolffConfig, } -func decodeConfig(cfg config.Provider) (conf minifyConfig, err error) { +func decodeConfig(cfg config.Provider) (conf MinifyConfig, err error) { conf = defaultConfig // May be set by CLI. diff --git a/minifiers/minifiers.go b/minifiers/minifiers.go index 5a5cec1217b..b06ef206d2d 100644 --- a/minifiers/minifiers.go +++ b/minifiers/minifiers.go @@ -108,7 +108,7 @@ func New(mediaTypes media.Types, outputFormats output.Formats, cfg config.Provid // getMinifier returns the appropriate minify.MinifierFunc for the MIME // type suffix s, given the config c. -func getMinifier(c minifyConfig, s string) minify.Minifier { +func getMinifier(c MinifyConfig, s string) minify.Minifier { switch { case s == "css" && !c.DisableCSS: return &c.Tdewolff.CSS diff --git a/output/layout.go b/output/layout.go index dcbdf461ac3..83fd5fbd3ee 100644 --- a/output/layout.go +++ b/output/layout.go @@ -16,8 +16,6 @@ package output import ( "strings" "sync" - - "github.com/gohugoio/hugo/helpers" ) // These may be used as content sections with potential conflicts. Avoid that. @@ -81,7 +79,7 @@ func (l *LayoutHandler) For(d LayoutDescriptor, f Format) ([]string, error) { layouts := resolvePageTemplate(d, f) - layouts = helpers.UniqueStringsReuse(layouts) + layouts = uniqueStringsReuse(layouts) l.mu.Lock() l.cache[key] = layouts @@ -300,3 +298,23 @@ func constructLayoutPath(typ, layout, variations, extension string) string { return p.String() } + +// Inline this here so we can use tinygo to compile a wasm binary of this package. +func uniqueStringsReuse(s []string) []string { + result := s[:0] + for i, val := range s { + var seen bool + + for j := 0; j < i; j++ { + if s[j] == val { + seen = true + break + } + } + + if !seen { + result = append(result, val) + } + } + return result +} diff --git a/output/outputFormat.go b/output/outputFormat.go index 0bc08e4905d..0f0f8494b60 100644 --- a/output/outputFormat.go +++ b/output/outputFormat.go @@ -17,12 +17,9 @@ package output import ( "encoding/json" "fmt" - "reflect" "sort" "strings" - "github.com/mitchellh/mapstructure" - "github.com/gohugoio/hugo/media" ) @@ -298,102 +295,6 @@ func (formats Formats) FromFilename(filename string) (f Format, found bool) { return } -// DecodeFormats takes a list of output format configurations and merges those, -// in the order given, with the Hugo defaults as the last resort. -func DecodeFormats(mediaTypes media.Types, maps ...map[string]any) (Formats, error) { - f := make(Formats, len(DefaultFormats)) - copy(f, DefaultFormats) - - for _, m := range maps { - for k, v := range m { - found := false - for i, vv := range f { - if strings.EqualFold(k, vv.Name) { - // Merge it with the existing - if err := decode(mediaTypes, v, &f[i]); err != nil { - return f, err - } - found = true - } - } - if !found { - var newOutFormat Format - newOutFormat.Name = k - if err := decode(mediaTypes, v, &newOutFormat); err != nil { - return f, err - } - - // We need values for these - if newOutFormat.BaseName == "" { - newOutFormat.BaseName = "index" - } - if newOutFormat.Rel == "" { - newOutFormat.Rel = "alternate" - } - - f = append(f, newOutFormat) - - } - } - } - - sort.Sort(f) - - return f, nil -} - -func decode(mediaTypes media.Types, input any, output *Format) error { - config := &mapstructure.DecoderConfig{ - Metadata: nil, - Result: output, - WeaklyTypedInput: true, - DecodeHook: func(a reflect.Type, b reflect.Type, c any) (any, error) { - if a.Kind() == reflect.Map { - dataVal := reflect.Indirect(reflect.ValueOf(c)) - for _, key := range dataVal.MapKeys() { - keyStr, ok := key.Interface().(string) - if !ok { - // Not a string key - continue - } - if strings.EqualFold(keyStr, "mediaType") { - // If mediaType is a string, look it up and replace it - // in the map. - vv := dataVal.MapIndex(key) - vvi := vv.Interface() - - switch vviv := vvi.(type) { - case media.Type: - // OK - case string: - mediaType, found := mediaTypes.GetByType(vviv) - if !found { - return c, fmt.Errorf("media type %q not found", vviv) - } - dataVal.SetMapIndex(key, reflect.ValueOf(mediaType)) - default: - return nil, fmt.Errorf("invalid output format configuration; wrong type for media type, expected string (e.g. text/html), got %T", vvi) - } - } - } - } - return c, nil - }, - } - - decoder, err := mapstructure.NewDecoder(config) - if err != nil { - return err - } - - if err = decoder.Decode(input); err != nil { - return fmt.Errorf("failed to decode output format configuration: %w", err) - } - - return nil - -} - // BaseFilename returns the base filename of f including an extension (ie. // "index.xml"). func (f Format) BaseFilename() string { diff --git a/output/outputconfig/config.go b/output/outputconfig/config.go new file mode 100644 index 00000000000..b5b4993481a --- /dev/null +++ b/output/outputconfig/config.go @@ -0,0 +1,121 @@ +// 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 outputconfig + +import ( + "fmt" + "reflect" + "sort" + "strings" + + "github.com/gohugoio/hugo/media" + "github.com/gohugoio/hugo/output" + "github.com/mitchellh/mapstructure" +) + +// DecodeFormats takes a list of output format configurations and merges those, +// in the order given, with the Hugo defaults as the last resort. +func DecodeFormats(mediaTypes media.Types, maps ...map[string]any) (output.Formats, error) { + f := make(output.Formats, len(output.DefaultFormats)) + copy(f, output.DefaultFormats) + + for _, m := range maps { + for k, v := range m { + found := false + for i, vv := range f { + if strings.EqualFold(k, vv.Name) { + // Merge it with the existing + if err := decode(mediaTypes, v, &f[i]); err != nil { + return f, err + } + found = true + } + } + if !found { + var newOutFormat output.Format + newOutFormat.Name = k + if err := decode(mediaTypes, v, &newOutFormat); err != nil { + return f, err + } + + // We need values for these + if newOutFormat.BaseName == "" { + newOutFormat.BaseName = "index" + } + if newOutFormat.Rel == "" { + newOutFormat.Rel = "alternate" + } + + f = append(f, newOutFormat) + + } + } + } + + sort.Sort(f) + + return f, nil +} + +func decode(mediaTypes media.Types, input any, output *output.Format) error { + config := &mapstructure.DecoderConfig{ + Metadata: nil, + Result: output, + WeaklyTypedInput: true, + DecodeHook: func(a reflect.Type, b reflect.Type, c any) (any, error) { + if a.Kind() == reflect.Map { + dataVal := reflect.Indirect(reflect.ValueOf(c)) + for _, key := range dataVal.MapKeys() { + keyStr, ok := key.Interface().(string) + if !ok { + // Not a string key + continue + } + if strings.EqualFold(keyStr, "mediaType") { + // If mediaType is a string, look it up and replace it + // in the map. + vv := dataVal.MapIndex(key) + vvi := vv.Interface() + + switch vviv := vvi.(type) { + case media.Type: + // OK + case string: + mediaType, found := mediaTypes.GetByType(vviv) + if !found { + return c, fmt.Errorf("media type %q not found", vviv) + } + dataVal.SetMapIndex(key, reflect.ValueOf(mediaType)) + default: + return nil, fmt.Errorf("invalid output format configuration; wrong type for media type, expected string (e.g. text/html), got %T", vvi) + } + } + } + } + return c, nil + }, + } + + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return err + } + + if err = decoder.Decode(input); err != nil { + return fmt.Errorf("failed to decode output format configuration: %w", err) + } + + return nil + +} diff --git a/related/inverted_index.go b/related/inverted_index.go index 1b1f69e3ec2..61cacb7fba8 100644 --- a/related/inverted_index.go +++ b/related/inverted_index.go @@ -42,25 +42,8 @@ var ( } ) -/* -Config is the top level configuration element used to configure how to retrieve -related content in Hugo. - -An example site config.toml: - - [related] - threshold = 1 - [[related.indices]] - name = "keywords" - weight = 200 - [[related.indices]] - name = "tags" - weight = 100 - [[related.indices]] - name = "date" - weight = 1 - pattern = "2006" -*/ +// Config is the top level configuration element used to configure how to retrieve +// related content in Hugo. type Config struct { // Only include matches >= threshold, a normalized rank between 0 and 100. Threshold int