// 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 media import ( "fmt" "path/filepath" "reflect" "sort" "strings" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/config" "github.com/mitchellh/mapstructure" "github.com/spf13/cast" "slices" ) // DefaultTypes is the default media types supported by Hugo. var DefaultTypes Types func init() { // Apply delimiter to all. for _, m := range defaultMediaTypesConfig { m.(map[string]any)["delimiter"] = "." } ns, err := DecodeTypes(nil) if err != nil { panic(err) } DefaultTypes = ns.Config // Initialize the Builtin types with values from DefaultTypes. v := reflect.ValueOf(&Builtin).Elem() for i := range v.NumField() { f := v.Field(i) fieldName := v.Type().Field(i).Name builtinType := f.Interface().(Type) if builtinType.Type == "" { panic(fmt.Errorf("builtin type %q is empty", fieldName)) } defaultType, found := DefaultTypes.GetByType(builtinType.Type) if !found { panic(fmt.Errorf("missing default type for field builtin type: %q", fieldName)) } f.Set(reflect.ValueOf(defaultType)) } } func init() { DefaultContentTypes = ContentTypes{ HTML: Builtin.HTMLType, Markdown: Builtin.MarkdownType, AsciiDoc: Builtin.AsciiDocType, Pandoc: Builtin.PandocType, ReStructuredText: Builtin.ReStructuredTextType, EmacsOrgMode: Builtin.EmacsOrgModeType, } DefaultContentTypes.init(nil) } var DefaultContentTypes ContentTypes type ContentTypeConfig struct { // Empty for now. } // ContentTypes holds the media types that are considered content in Hugo. type ContentTypes struct { HTML Type Markdown Type AsciiDoc Type Pandoc Type ReStructuredText Type EmacsOrgMode Type types Types // Created in init(). extensionSet map[string]bool } func (t *ContentTypes) init(types Types) { sort.Slice(t.types, func(i, j int) bool { return t.types[i].Type < t.types[j].Type }) if tt, ok := types.GetByType(t.HTML.Type); ok { t.HTML = tt } if tt, ok := types.GetByType(t.Markdown.Type); ok { t.Markdown = tt } if tt, ok := types.GetByType(t.AsciiDoc.Type); ok { t.AsciiDoc = tt } if tt, ok := types.GetByType(t.Pandoc.Type); ok { t.Pandoc = tt } if tt, ok := types.GetByType(t.ReStructuredText.Type); ok { t.ReStructuredText = tt } if tt, ok := types.GetByType(t.EmacsOrgMode.Type); ok { t.EmacsOrgMode = tt } t.extensionSet = make(map[string]bool) for _, mt := range t.types { for _, suffix := range mt.Suffixes() { t.extensionSet[suffix] = true } } } func (t ContentTypes) IsContentSuffix(suffix string) bool { return t.extensionSet[suffix] } // IsContentFile returns whether the given filename is a content file. func (t ContentTypes) IsContentFile(filename string) bool { return t.IsContentSuffix(strings.TrimPrefix(filepath.Ext(filename), ".")) } // IsIndexContentFile returns whether the given filename is an index content file. func (t ContentTypes) IsIndexContentFile(filename string) bool { if !t.IsContentFile(filename) { return false } base := filepath.Base(filename) return strings.HasPrefix(base, "index.") || strings.HasPrefix(base, "_index.") } // IsHTMLSuffix returns whether the given suffix is a HTML media type. func (t ContentTypes) IsHTMLSuffix(suffix string) bool { return slices.Contains(t.HTML.Suffixes(), suffix) } // Types is a slice of media types. func (t ContentTypes) Types() Types { return t.types } // Hold the configuration for a given media type. type MediaTypeConfig struct { // The file suffixes used for this media type. Suffixes []string // Delimiter used before suffix. Delimiter string } var defaultContentTypesConfig = map[string]ContentTypeConfig{ Builtin.HTMLType.Type: {}, Builtin.MarkdownType.Type: {}, Builtin.AsciiDocType.Type: {}, Builtin.PandocType.Type: {}, Builtin.ReStructuredTextType.Type: {}, Builtin.EmacsOrgModeType.Type: {}, } // DecodeContentTypes decodes the given map of content types. func DecodeContentTypes(in map[string]any, types Types) (*config.ConfigNamespace[map[string]ContentTypeConfig, ContentTypes], error) { buildConfig := func(v any) (ContentTypes, any, error) { var s map[string]ContentTypeConfig c := DefaultContentTypes m, err := maps.ToStringMapE(v) if err != nil { return c, nil, err } if len(m) == 0 { s = defaultContentTypesConfig } else { s = make(map[string]ContentTypeConfig) m = maps.CleanConfigStringMap(m) for k, v := range m { var ctc ContentTypeConfig if err := mapstructure.WeakDecode(v, &ctc); err != nil { return c, nil, err } s[k] = ctc } } for k := range s { mediaType, found := types.GetByType(k) if !found { return c, nil, fmt.Errorf("unknown media type %q", k) } c.types = append(c.types, mediaType) } c.init(types) return c, s, nil } ns, err := config.DecodeNamespace[map[string]ContentTypeConfig](in, buildConfig) if err != nil { return nil, fmt.Errorf("failed to decode media types: %w", err) } return ns, nil } // DecodeTypes decodes the given map of media types. func DecodeTypes(in map[string]any) (*config.ConfigNamespace[map[string]MediaTypeConfig, Types], error) { buildConfig := func(v any) (Types, any, error) { m, err := maps.ToStringMapE(v) if err != nil { return nil, nil, err } if m == nil { m = map[string]any{} } m = maps.CleanConfigStringMap(m) // Merge with defaults. maps.MergeShallow(m, defaultMediaTypesConfig) var types Types for k, v := range m { mediaType, err := FromString(k) if err != nil { return nil, nil, err } if err := mapstructure.WeakDecode(v, &mediaType); err != nil { return nil, nil, err } mm := maps.ToStringMap(v) suffixes, _, found := maps.LookupEqualFold(mm, "suffixes") if found { mediaType.SuffixesCSV = strings.TrimSpace(strings.ToLower(strings.Join(cast.ToStringSlice(suffixes), ","))) } if mediaType.SuffixesCSV != "" && mediaType.Delimiter == "" { mediaType.Delimiter = DefaultDelimiter } InitMediaType(&mediaType) types = append(types, mediaType) } sort.Sort(types) return types, m, nil } ns, err := config.DecodeNamespace[map[string]MediaTypeConfig](in, buildConfig) if err != nil { return nil, fmt.Errorf("failed to decode media types: %w", err) } return ns, nil } // TODO(bep) get rid of this. var DefaultPathParser = &paths.PathParser{ IsContentExt: func(ext string) bool { panic("not supported") }, }