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)
}