Add ContentTypes to config - hugo - [fork] hugo port for 9front
 (HTM) git clone git@git.drkhsh.at/hugo.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit c2fb22120924b66ba6d89dacca6487ff12e78e0f
 (DIR) parent 4245a4514d58c0896900bc242390b31e92005d43
 (HTM) Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
       Date:   Fri,  7 Feb 2025 10:29:35 +0100
       
       Add ContentTypes to config
       
       This is an empty struct for now, but we will most likely expand on that.
       
       ```
       [contentTypes]
         [contentTypes.'text/markdown']
       ```
       
       The above means that only Markdown will be considered a content type. E.g. HTML will be treated as plain text.
       
       Fixes #12274
       
       Diffstat:
         M common/hreflect/helpers.go          |      10 ++++++++++
         M config/allconfig/allconfig.go       |       7 ++++---
         M config/allconfig/allconfig_integra… |      21 ++++++++++++++++++++-
         M config/allconfig/alldecoders.go     |       9 +++++++++
         M config/allconfig/configlanguage.go  |       2 +-
         M hugolib/content_map_test.go         |      48 +++++++++++++++++++++++++++++++
         M media/config.go                     |     115 ++++++++++++++++++++++---------
         M media/mediaType_test.go             |       8 --------
         M parser/lowercase_camel_json.go      |       2 +-
         M resources/page/page_nop.go          |       5 +----
         M source/fileInfo.go                  |       1 +
         M tpl/reflect/reflect.go              |       6 +++---
       
       12 files changed, 182 insertions(+), 52 deletions(-)
       ---
 (DIR) diff --git a/common/hreflect/helpers.go b/common/hreflect/helpers.go
       @@ -74,6 +74,16 @@ func IsTruthful(in any) bool {
                }
        }
        
       +// IsMap reports whether v is a map.
       +func IsMap(v any) bool {
       +        return reflect.ValueOf(v).Kind() == reflect.Map
       +}
       +
       +// IsSlice reports whether v is a slice.
       +func IsSlice(v any) bool {
       +        return reflect.ValueOf(v).Kind() == reflect.Slice
       +}
       +
        var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
        
        // IsTruthfulValue returns whether the given value has a meaningful truth value.
 (DIR) diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go
       @@ -128,6 +128,9 @@ type Config struct {
                // <docsmeta>{"identifiers": ["markup"] }</docsmeta>
                Markup markup_config.Config `mapstructure:"-"`
        
       +        // ContentTypes are the media types that's considered content in Hugo.
       +        ContentTypes *config.ConfigNamespace[map[string]media.ContentTypeConfig, media.ContentTypes] `mapstructure:"-"`
       +
                // The mediatypes configuration section maps the MIME type (a string) to a configuration object for that type.
                // <docsmeta>{"identifiers": ["mediatypes"], "refs": ["types:media:type"] }</docsmeta>
                MediaTypes *config.ConfigNamespace[map[string]media.MediaTypeConfig, media.Types] `mapstructure:"-"`
       @@ -433,7 +436,6 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
                        IgnoredLogs:         ignoredLogIDs,
                        KindOutputFormats:   kindOutputFormats,
                        DefaultOutputFormat: defaultOutputFormat,
       -                ContentTypes:        media.DefaultContentTypes.FromTypes(c.MediaTypes.Config),
                        CreateTitle:         helpers.GetTitleFunc(c.TitleCaseStyle),
                        IsUglyURLSection:    isUglyURL,
                        IgnoreFile:          ignoreFile,
       @@ -471,7 +473,6 @@ type ConfigCompiled struct {
                ServerInterface     string
                KindOutputFormats   map[string]output.Formats
                DefaultOutputFormat output.Format
       -        ContentTypes        media.ContentTypes
                DisabledKinds       map[string]bool
                DisabledLanguages   map[string]bool
                IgnoredLogs         map[string]bool
       @@ -839,7 +840,7 @@ func (c *Configs) Init() error {
                c.Languages = languages
                c.LanguagesDefaultFirst = languagesDefaultFirst
        
       -        c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.C.ContentTypes.IsContentSuffix}
       +        c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix}
        
                c.configLangs = make([]config.AllProvider, len(c.Languages))
                for i, l := range c.LanguagesDefaultFirst {
 (DIR) diff --git a/config/allconfig/allconfig_integration_test.go b/config/allconfig/allconfig_integration_test.go
       @@ -7,6 +7,7 @@ import (
                qt "github.com/frankban/quicktest"
                "github.com/gohugoio/hugo/config/allconfig"
                "github.com/gohugoio/hugo/hugolib"
       +        "github.com/gohugoio/hugo/media"
        )
        
        func TestDirsMount(t *testing.T) {
       @@ -97,7 +98,7 @@ suffixes = ["html", "xhtml"]
                b := hugolib.Test(t, files)
        
                conf := b.H.Configs.Base
       -        contentTypes := conf.C.ContentTypes
       +        contentTypes := conf.ContentTypes.Config
        
                b.Assert(contentTypes.HTML.Suffixes(), qt.DeepEquals, []string{"html", "xhtml"})
                b.Assert(contentTypes.Markdown.Suffixes(), qt.DeepEquals, []string{"md", "mdown", "markdown"})
       @@ -215,3 +216,21 @@ weight = 3
                b := hugolib.Test(t, files)
                b.Assert(b.H.Configs.LanguageConfigSlice[0].Title, qt.Equals, `TITLE_DE`)
        }
       +
       +func TestContentTypesDefault(t *testing.T) {
       +        files := `
       +-- hugo.toml --
       +baseURL = "https://example.com"
       +
       +
       +`
       +
       +        b := hugolib.Test(t, files)
       +
       +        ct := b.H.Configs.Base.ContentTypes
       +        c := ct.Config
       +        s := ct.SourceStructure.(map[string]media.ContentTypeConfig)
       +
       +        b.Assert(c.IsContentFile("foo.md"), qt.Equals, true)
       +        b.Assert(len(s), qt.Equals, 6)
       +}
 (DIR) diff --git a/config/allconfig/alldecoders.go b/config/allconfig/alldecoders.go
       @@ -163,6 +163,15 @@ var allDecoderSetups = map[string]decodeWeight{
                                return err
                        },
                },
       +        "contenttypes": {
       +                key:    "contenttypes",
       +                weight: 100, // This needs to be decoded after media types.
       +                decode: func(d decodeWeight, p decodeConfig) error {
       +                        var err error
       +                        p.c.ContentTypes, err = media.DecodeContentTypes(p.p.GetStringMap(d.key), p.c.MediaTypes.Config)
       +                        return err
       +                },
       +        },
                "mediatypes": {
                        key: "mediatypes",
                        decode: func(d decodeWeight, p decodeConfig) error {
 (DIR) diff --git a/config/allconfig/configlanguage.go b/config/allconfig/configlanguage.go
       @@ -145,7 +145,7 @@ func (c ConfigLanguage) NewIdentityManager(name string, opts ...identity.Manager
        }
        
        func (c ConfigLanguage) ContentTypes() config.ContentTypesProvider {
       -        return c.config.C.ContentTypes
       +        return c.config.ContentTypes.Config
        }
        
        // GetConfigSection is mostly used in tests. The switch statement isn't complete, but what's in use.
 (DIR) diff --git a/hugolib/content_map_test.go b/hugolib/content_map_test.go
       @@ -501,3 +501,51 @@ func (n *testContentNode) resetBuildState() {
        
        func (n *testContentNode) MarkStale() {
        }
       +
       +// Issue 12274.
       +func TestHTMLNotContent(t *testing.T) {
       +        filesTemplate := `
       +-- hugo.toml.temp --
       +[contentTypes]
       +[contentTypes."text/markdown"]
       +# Emopty for now.
       +-- hugo.yaml.temp --
       +contentTypes:
       +  text/markdown: {}
       +-- hugo.json.temp --
       +{
       +  "contentTypes": {
       +    "text/markdown": {}
       +  }
       +}
       +-- content/p1/index.md --
       +---
       +title: p1
       +---
       +-- content/p1/a.html --
       +<p>a</p>
       +-- content/p1/b.html --
       +<p>b</p>
       +-- content/p1/c.html --
       +<p>c</p>
       +-- layouts/_default/single.html --
       +|{{ (.Resources.Get "a.html").RelPermalink -}}
       +|{{ (.Resources.Get "b.html").RelPermalink -}}
       +|{{ (.Resources.Get "c.html").Publish }}
       +`
       +
       +        for _, format := range []string{"toml", "yaml", "json"} {
       +                format := format
       +                t.Run(format, func(t *testing.T) {
       +                        t.Parallel()
       +
       +                        files := strings.Replace(filesTemplate, format+".temp", format, 1)
       +                        b := Test(t, files)
       +
       +                        b.AssertFileContent("public/p1/index.html", "|/p1/a.html|/p1/b.html|")
       +                        b.AssertFileContent("public/p1/a.html", "<p>a</p>")
       +                        b.AssertFileContent("public/p1/b.html", "<p>b</p>")
       +                        b.AssertFileContent("public/p1/c.html", "<p>c</p>")
       +                })
       +        }
       +}
 (DIR) diff --git a/media/config.go b/media/config.go
       @@ -71,11 +71,15 @@ func init() {
                        EmacsOrgMode:     Builtin.EmacsOrgModeType,
                }
        
       -        DefaultContentTypes.init()
       +        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
       @@ -85,13 +89,36 @@ type ContentTypes struct {
                ReStructuredText Type
                EmacsOrgMode     Type
        
       +        types Types
       +
                // Created in init().
       -        types        Types
                extensionSet map[string]bool
        }
        
       -func (t *ContentTypes) init() {
       -        t.types = Types{t.HTML, t.Markdown, t.AsciiDoc, t.Pandoc, t.ReStructuredText, t.EmacsOrgMode}
       +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() {
       @@ -135,32 +162,6 @@ func (t ContentTypes) Types() Types {
                return t.types
        }
        
       -// FromTypes creates a new ContentTypes updated with the values from the given Types.
       -func (t ContentTypes) FromTypes(types Types) ContentTypes {
       -        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.init()
       -
       -        return t
       -}
       -
        // Hold the configuration for a given media type.
        type MediaTypeConfig struct {
                // The file suffixes used for this media type.
       @@ -169,6 +170,58 @@ type MediaTypeConfig struct {
                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) {
       @@ -220,6 +273,6 @@ func DecodeTypes(in map[string]any) (*config.ConfigNamespace[map[string]MediaTyp
        // TODO(bep) get rid of this.
        var DefaultPathParser = &paths.PathParser{
                IsContentExt: func(ext string) bool {
       -                return DefaultContentTypes.IsContentSuffix(ext)
       +                panic("not supported")
                },
        }
 (DIR) diff --git a/media/mediaType_test.go b/media/mediaType_test.go
       @@ -214,11 +214,3 @@ func BenchmarkTypeOps(b *testing.B) {
        
                }
        }
       -
       -func TestIsContentFile(t *testing.T) {
       -        c := qt.New(t)
       -
       -        c.Assert(DefaultContentTypes.IsContentFile(filepath.FromSlash("my/file.md")), qt.Equals, true)
       -        c.Assert(DefaultContentTypes.IsContentFile(filepath.FromSlash("my/file.ad")), qt.Equals, true)
       -        c.Assert(DefaultContentTypes.IsContentFile(filepath.FromSlash("textfile.txt")), qt.Equals, false)
       -}
 (DIR) diff --git a/parser/lowercase_camel_json.go b/parser/lowercase_camel_json.go
       @@ -107,7 +107,7 @@ func (c ReplacingJSONMarshaller) MarshalJSON() ([]byte, error) {
                        var removeZeroVAlues func(m map[string]any)
                        removeZeroVAlues = func(m map[string]any) {
                                for k, v := range m {
       -                                if !hreflect.IsTruthful(v) {
       +                                if !hreflect.IsMap(v) && !hreflect.IsTruthful(v) {
                                                delete(m, k)
                                        } else {
                                                switch vv := v.(type) {
 (DIR) diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go
       @@ -21,7 +21,6 @@ import (
                "html/template"
                "time"
        
       -        "github.com/gohugoio/hugo/hugofs/files"
                "github.com/gohugoio/hugo/markup/converter"
                "github.com/gohugoio/hugo/markup/tableofcontents"
        
       @@ -59,8 +58,6 @@ var (
        // PageNop implements Page, but does nothing.
        type nopPage int
        
       -var noOpPathInfo = media.DefaultPathParser.Parse(files.ComponentFolderContent, "no-op.md")
       -
        func (p *nopPage) Aliases() []string {
                return nil
        }
       @@ -338,7 +335,7 @@ func (p *nopPage) Path() string {
        }
        
        func (p *nopPage) PathInfo() *paths.Path {
       -        return noOpPathInfo
       +        return nil
        }
        
        func (p *nopPage) Permalink() string {
 (DIR) diff --git a/source/fileInfo.go b/source/fileInfo.go
       @@ -132,6 +132,7 @@ func (fi *File) p() *paths.Path {
                return fi.fim.Meta().PathInfo.Unnormalized()
        }
        
       +// Used in tests.
        func NewFileInfoFrom(path, filename string) *File {
                meta := &hugofs.FileMeta{
                        Filename: filename,
 (DIR) diff --git a/tpl/reflect/reflect.go b/tpl/reflect/reflect.go
       @@ -14,7 +14,7 @@
        package reflect
        
        import (
       -        "reflect"
       +        "github.com/gohugoio/hugo/common/hreflect"
        )
        
        // New returns a new instance of the reflect-namespaced template functions.
       @@ -27,10 +27,10 @@ type Namespace struct{}
        
        // IsMap reports whether v is a map.
        func (ns *Namespace) IsMap(v any) bool {
       -        return reflect.ValueOf(v).Kind() == reflect.Map
       +        return hreflect.IsMap(v)
        }
        
        // IsSlice reports whether v is a slice.
        func (ns *Namespace) IsSlice(v any) bool {
       -        return reflect.ValueOf(v).Kind() == reflect.Slice
       +        return hreflect.IsSlice(v)
        }