diff --git a/common/hreflect/helpers.go b/common/hreflect/helpers.go index fc83165c7..4d7339b5b 100644 --- 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. diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go index 3c2eddcab..3021028fd 100644 --- a/config/allconfig/allconfig.go +++ b/config/allconfig/allconfig.go @@ -128,6 +128,9 @@ type Config struct { // {"identifiers": ["markup"] } 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. // {"identifiers": ["mediatypes"], "refs": ["types:media:type"] } 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 { diff --git a/config/allconfig/allconfig_integration_test.go b/config/allconfig/allconfig_integration_test.go index 162a36cdb..cae04ba85 100644 --- 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) +} diff --git a/config/allconfig/alldecoders.go b/config/allconfig/alldecoders.go index 60e571999..0bf8508d9 100644 --- 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 { diff --git a/config/allconfig/configlanguage.go b/config/allconfig/configlanguage.go index deec61449..6990a3590 100644 --- 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. diff --git a/hugolib/content_map_test.go b/hugolib/content_map_test.go index 6a1245846..4cbdaf53a 100644 --- 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 -- +

a

+-- content/p1/b.html -- +

b

+-- content/p1/c.html -- +

c

+-- 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", "

a

") + b.AssertFileContent("public/p1/b.html", "

b

") + b.AssertFileContent("public/p1/c.html", "

c

") + }) + } +} diff --git a/media/config.go b/media/config.go index e00837e5e..e50d8499d 100644 --- 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") }, } diff --git a/media/mediaType_test.go b/media/mediaType_test.go index fb3eb664f..3b8e099b8 100644 --- 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) -} diff --git a/parser/lowercase_camel_json.go b/parser/lowercase_camel_json.go index c61a4078e..468c1a8fe 100644 --- 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) { diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go index af9f2682d..398a7df02 100644 --- 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 { diff --git a/source/fileInfo.go b/source/fileInfo.go index 8994dec97..8403c8088 100644 --- 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, diff --git a/tpl/reflect/reflect.go b/tpl/reflect/reflect.go index 07834be1c..c19c8c178 100644 --- 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) }