]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Add ContentTypes to config
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 7 Feb 2025 09:29:35 +0000 (10:29 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 8 Feb 2025 09:31:15 +0000 (10:31 +0100)
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

12 files changed:
common/hreflect/helpers.go
config/allconfig/allconfig.go
config/allconfig/allconfig_integration_test.go
config/allconfig/alldecoders.go
config/allconfig/configlanguage.go
hugolib/content_map_test.go
media/config.go
media/mediaType_test.go
parser/lowercase_camel_json.go
resources/page/page_nop.go
source/fileInfo.go
tpl/reflect/reflect.go

index fc83165c7cb19cee80564c11bea97d6f7f38dfce..4d7339b5bae7e27f93e31fa5f9bbd3ee93e165c6 100644 (file)
@@ -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.
index 3c2eddcaba3cd5354111ebcfbbffbebe7889ad35..3021028fdfd878ca1ce7e9ffb6bdb1aa340b5e29 100644 (file)
@@ -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 {
index 162a36cdb89e4f82c3ef7571ffa61f68b825a523..cae04ba8546a11a55bca42430abdefd39f2fb967 100644 (file)
@@ -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)
+}
index 60e571999408400501b00d0ab476c7f3e5b0dcaa..0bf8508d96cef6b4d2eee3c5a410e2e4dd12c0b3 100644 (file)
@@ -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 {
index deec61449bb1ffd008eae29b4181f84ea1ea7311..6990a35904a370d0e954bdfd03aa8e342ca52573 100644 (file)
@@ -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.
index 6a12458462fb4c69d7f9611b53d52744b5a4ea29..4cbdaf53a48225d70b5479f502fe082c242f84d8 100644 (file)
@@ -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>")
+               })
+       }
+}
index e00837e5e08d611f9d31473fa9284c3c8d246be4..e50d8499d448b593c7126d8678e2d12a5840d994 100644 (file)
@@ -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")
        },
 }
index fb3eb664f9eb8eac5b3b9bbf84011a9084b27fb1..3b8e099b897255dd6a269ec25dff1d82a3623a44 100644 (file)
@@ -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)
-}
index c61a4078ec52f1acf28661acad726a7660a6e6d2..468c1a8fee8f4dac15a3337814978702c808977a 100644 (file)
@@ -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) {
index af9f2682d37db019ad9a41344288b35190da8880..398a7df02cef43c86d81645a2a7df6d8eaf1e794 100644 (file)
@@ -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 {
index 8994dec976cdb61844e15f9961d43c78594c0f2f..8403c8088940832bb5fd57f80b27f19122cf3bdc 100644 (file)
@@ -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,
index 07834be1c8195754c8442775508bd150fa5eb42d..c19c8c178a8fe5c9107ba3f4cade488edfe6377b 100644 (file)
@@ -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)
 }