Skip to content

Commit c2fb221

Browse files
committed
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
1 parent 4245a45 commit c2fb221

File tree

12 files changed

+182
-52
lines changed

12 files changed

+182
-52
lines changed

common/hreflect/helpers.go

+10
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ func IsTruthful(in any) bool {
7474
}
7575
}
7676

77+
// IsMap reports whether v is a map.
78+
func IsMap(v any) bool {
79+
return reflect.ValueOf(v).Kind() == reflect.Map
80+
}
81+
82+
// IsSlice reports whether v is a slice.
83+
func IsSlice(v any) bool {
84+
return reflect.ValueOf(v).Kind() == reflect.Slice
85+
}
86+
7787
var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
7888

7989
// IsTruthfulValue returns whether the given value has a meaningful truth value.

config/allconfig/allconfig.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ type Config struct {
128128
// <docsmeta>{"identifiers": ["markup"] }</docsmeta>
129129
Markup markup_config.Config `mapstructure:"-"`
130130

131+
// ContentTypes are the media types that's considered content in Hugo.
132+
ContentTypes *config.ConfigNamespace[map[string]media.ContentTypeConfig, media.ContentTypes] `mapstructure:"-"`
133+
131134
// The mediatypes configuration section maps the MIME type (a string) to a configuration object for that type.
132135
// <docsmeta>{"identifiers": ["mediatypes"], "refs": ["types:media:type"] }</docsmeta>
133136
MediaTypes *config.ConfigNamespace[map[string]media.MediaTypeConfig, media.Types] `mapstructure:"-"`
@@ -433,7 +436,6 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
433436
IgnoredLogs: ignoredLogIDs,
434437
KindOutputFormats: kindOutputFormats,
435438
DefaultOutputFormat: defaultOutputFormat,
436-
ContentTypes: media.DefaultContentTypes.FromTypes(c.MediaTypes.Config),
437439
CreateTitle: helpers.GetTitleFunc(c.TitleCaseStyle),
438440
IsUglyURLSection: isUglyURL,
439441
IgnoreFile: ignoreFile,
@@ -471,7 +473,6 @@ type ConfigCompiled struct {
471473
ServerInterface string
472474
KindOutputFormats map[string]output.Formats
473475
DefaultOutputFormat output.Format
474-
ContentTypes media.ContentTypes
475476
DisabledKinds map[string]bool
476477
DisabledLanguages map[string]bool
477478
IgnoredLogs map[string]bool
@@ -839,7 +840,7 @@ func (c *Configs) Init() error {
839840
c.Languages = languages
840841
c.LanguagesDefaultFirst = languagesDefaultFirst
841842

842-
c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.C.ContentTypes.IsContentSuffix}
843+
c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix}
843844

844845
c.configLangs = make([]config.AllProvider, len(c.Languages))
845846
for i, l := range c.LanguagesDefaultFirst {

config/allconfig/allconfig_integration_test.go

+20-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
qt "github.com/frankban/quicktest"
88
"github.com/gohugoio/hugo/config/allconfig"
99
"github.com/gohugoio/hugo/hugolib"
10+
"github.com/gohugoio/hugo/media"
1011
)
1112

1213
func TestDirsMount(t *testing.T) {
@@ -97,7 +98,7 @@ suffixes = ["html", "xhtml"]
9798
b := hugolib.Test(t, files)
9899

99100
conf := b.H.Configs.Base
100-
contentTypes := conf.C.ContentTypes
101+
contentTypes := conf.ContentTypes.Config
101102

102103
b.Assert(contentTypes.HTML.Suffixes(), qt.DeepEquals, []string{"html", "xhtml"})
103104
b.Assert(contentTypes.Markdown.Suffixes(), qt.DeepEquals, []string{"md", "mdown", "markdown"})
@@ -215,3 +216,21 @@ weight = 3
215216
b := hugolib.Test(t, files)
216217
b.Assert(b.H.Configs.LanguageConfigSlice[0].Title, qt.Equals, `TITLE_DE`)
217218
}
219+
220+
func TestContentTypesDefault(t *testing.T) {
221+
files := `
222+
-- hugo.toml --
223+
baseURL = "https://example.com"
224+
225+
226+
`
227+
228+
b := hugolib.Test(t, files)
229+
230+
ct := b.H.Configs.Base.ContentTypes
231+
c := ct.Config
232+
s := ct.SourceStructure.(map[string]media.ContentTypeConfig)
233+
234+
b.Assert(c.IsContentFile("foo.md"), qt.Equals, true)
235+
b.Assert(len(s), qt.Equals, 6)
236+
}

config/allconfig/alldecoders.go

+9
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,15 @@ var allDecoderSetups = map[string]decodeWeight{
163163
return err
164164
},
165165
},
166+
"contenttypes": {
167+
key: "contenttypes",
168+
weight: 100, // This needs to be decoded after media types.
169+
decode: func(d decodeWeight, p decodeConfig) error {
170+
var err error
171+
p.c.ContentTypes, err = media.DecodeContentTypes(p.p.GetStringMap(d.key), p.c.MediaTypes.Config)
172+
return err
173+
},
174+
},
166175
"mediatypes": {
167176
key: "mediatypes",
168177
decode: func(d decodeWeight, p decodeConfig) error {

config/allconfig/configlanguage.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func (c ConfigLanguage) NewIdentityManager(name string, opts ...identity.Manager
145145
}
146146

147147
func (c ConfigLanguage) ContentTypes() config.ContentTypesProvider {
148-
return c.config.C.ContentTypes
148+
return c.config.ContentTypes.Config
149149
}
150150

151151
// GetConfigSection is mostly used in tests. The switch statement isn't complete, but what's in use.

hugolib/content_map_test.go

+48
Original file line numberDiff line numberDiff line change
@@ -501,3 +501,51 @@ func (n *testContentNode) resetBuildState() {
501501

502502
func (n *testContentNode) MarkStale() {
503503
}
504+
505+
// Issue 12274.
506+
func TestHTMLNotContent(t *testing.T) {
507+
filesTemplate := `
508+
-- hugo.toml.temp --
509+
[contentTypes]
510+
[contentTypes."text/markdown"]
511+
# Emopty for now.
512+
-- hugo.yaml.temp --
513+
contentTypes:
514+
text/markdown: {}
515+
-- hugo.json.temp --
516+
{
517+
"contentTypes": {
518+
"text/markdown": {}
519+
}
520+
}
521+
-- content/p1/index.md --
522+
---
523+
title: p1
524+
---
525+
-- content/p1/a.html --
526+
<p>a</p>
527+
-- content/p1/b.html --
528+
<p>b</p>
529+
-- content/p1/c.html --
530+
<p>c</p>
531+
-- layouts/_default/single.html --
532+
|{{ (.Resources.Get "a.html").RelPermalink -}}
533+
|{{ (.Resources.Get "b.html").RelPermalink -}}
534+
|{{ (.Resources.Get "c.html").Publish }}
535+
`
536+
537+
for _, format := range []string{"toml", "yaml", "json"} {
538+
format := format
539+
t.Run(format, func(t *testing.T) {
540+
t.Parallel()
541+
542+
files := strings.Replace(filesTemplate, format+".temp", format, 1)
543+
b := Test(t, files)
544+
545+
b.AssertFileContent("public/p1/index.html", "|/p1/a.html|/p1/b.html|")
546+
b.AssertFileContent("public/p1/a.html", "<p>a</p>")
547+
b.AssertFileContent("public/p1/b.html", "<p>b</p>")
548+
b.AssertFileContent("public/p1/c.html", "<p>c</p>")
549+
})
550+
}
551+
}

media/config.go

+84-31
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,15 @@ func init() {
7171
EmacsOrgMode: Builtin.EmacsOrgModeType,
7272
}
7373

74-
DefaultContentTypes.init()
74+
DefaultContentTypes.init(nil)
7575
}
7676

7777
var DefaultContentTypes ContentTypes
7878

79+
type ContentTypeConfig struct {
80+
// Empty for now.
81+
}
82+
7983
// ContentTypes holds the media types that are considered content in Hugo.
8084
type ContentTypes struct {
8185
HTML Type
@@ -85,13 +89,36 @@ type ContentTypes struct {
8589
ReStructuredText Type
8690
EmacsOrgMode Type
8791

92+
types Types
93+
8894
// Created in init().
89-
types Types
9095
extensionSet map[string]bool
9196
}
9297

93-
func (t *ContentTypes) init() {
94-
t.types = Types{t.HTML, t.Markdown, t.AsciiDoc, t.Pandoc, t.ReStructuredText, t.EmacsOrgMode}
98+
func (t *ContentTypes) init(types Types) {
99+
sort.Slice(t.types, func(i, j int) bool {
100+
return t.types[i].Type < t.types[j].Type
101+
})
102+
103+
if tt, ok := types.GetByType(t.HTML.Type); ok {
104+
t.HTML = tt
105+
}
106+
if tt, ok := types.GetByType(t.Markdown.Type); ok {
107+
t.Markdown = tt
108+
}
109+
if tt, ok := types.GetByType(t.AsciiDoc.Type); ok {
110+
t.AsciiDoc = tt
111+
}
112+
if tt, ok := types.GetByType(t.Pandoc.Type); ok {
113+
t.Pandoc = tt
114+
}
115+
if tt, ok := types.GetByType(t.ReStructuredText.Type); ok {
116+
t.ReStructuredText = tt
117+
}
118+
if tt, ok := types.GetByType(t.EmacsOrgMode.Type); ok {
119+
t.EmacsOrgMode = tt
120+
}
121+
95122
t.extensionSet = make(map[string]bool)
96123
for _, mt := range t.types {
97124
for _, suffix := range mt.Suffixes() {
@@ -135,32 +162,6 @@ func (t ContentTypes) Types() Types {
135162
return t.types
136163
}
137164

138-
// FromTypes creates a new ContentTypes updated with the values from the given Types.
139-
func (t ContentTypes) FromTypes(types Types) ContentTypes {
140-
if tt, ok := types.GetByType(t.HTML.Type); ok {
141-
t.HTML = tt
142-
}
143-
if tt, ok := types.GetByType(t.Markdown.Type); ok {
144-
t.Markdown = tt
145-
}
146-
if tt, ok := types.GetByType(t.AsciiDoc.Type); ok {
147-
t.AsciiDoc = tt
148-
}
149-
if tt, ok := types.GetByType(t.Pandoc.Type); ok {
150-
t.Pandoc = tt
151-
}
152-
if tt, ok := types.GetByType(t.ReStructuredText.Type); ok {
153-
t.ReStructuredText = tt
154-
}
155-
if tt, ok := types.GetByType(t.EmacsOrgMode.Type); ok {
156-
t.EmacsOrgMode = tt
157-
}
158-
159-
t.init()
160-
161-
return t
162-
}
163-
164165
// Hold the configuration for a given media type.
165166
type MediaTypeConfig struct {
166167
// The file suffixes used for this media type.
@@ -169,6 +170,58 @@ type MediaTypeConfig struct {
169170
Delimiter string
170171
}
171172

173+
var defaultContentTypesConfig = map[string]ContentTypeConfig{
174+
Builtin.HTMLType.Type: {},
175+
Builtin.MarkdownType.Type: {},
176+
Builtin.AsciiDocType.Type: {},
177+
Builtin.PandocType.Type: {},
178+
Builtin.ReStructuredTextType.Type: {},
179+
Builtin.EmacsOrgModeType.Type: {},
180+
}
181+
182+
// DecodeContentTypes decodes the given map of content types.
183+
func DecodeContentTypes(in map[string]any, types Types) (*config.ConfigNamespace[map[string]ContentTypeConfig, ContentTypes], error) {
184+
buildConfig := func(v any) (ContentTypes, any, error) {
185+
var s map[string]ContentTypeConfig
186+
c := DefaultContentTypes
187+
m, err := maps.ToStringMapE(v)
188+
if err != nil {
189+
return c, nil, err
190+
}
191+
if len(m) == 0 {
192+
s = defaultContentTypesConfig
193+
} else {
194+
s = make(map[string]ContentTypeConfig)
195+
m = maps.CleanConfigStringMap(m)
196+
for k, v := range m {
197+
var ctc ContentTypeConfig
198+
if err := mapstructure.WeakDecode(v, &ctc); err != nil {
199+
return c, nil, err
200+
}
201+
s[k] = ctc
202+
}
203+
}
204+
205+
for k := range s {
206+
mediaType, found := types.GetByType(k)
207+
if !found {
208+
return c, nil, fmt.Errorf("unknown media type %q", k)
209+
}
210+
c.types = append(c.types, mediaType)
211+
}
212+
213+
c.init(types)
214+
215+
return c, s, nil
216+
}
217+
218+
ns, err := config.DecodeNamespace[map[string]ContentTypeConfig](in, buildConfig)
219+
if err != nil {
220+
return nil, fmt.Errorf("failed to decode media types: %w", err)
221+
}
222+
return ns, nil
223+
}
224+
172225
// DecodeTypes decodes the given map of media types.
173226
func DecodeTypes(in map[string]any) (*config.ConfigNamespace[map[string]MediaTypeConfig, Types], error) {
174227
buildConfig := func(v any) (Types, any, error) {
@@ -220,6 +273,6 @@ func DecodeTypes(in map[string]any) (*config.ConfigNamespace[map[string]MediaTyp
220273
// TODO(bep) get rid of this.
221274
var DefaultPathParser = &paths.PathParser{
222275
IsContentExt: func(ext string) bool {
223-
return DefaultContentTypes.IsContentSuffix(ext)
276+
panic("not supported")
224277
},
225278
}

media/mediaType_test.go

-8
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,3 @@ func BenchmarkTypeOps(b *testing.B) {
214214

215215
}
216216
}
217-
218-
func TestIsContentFile(t *testing.T) {
219-
c := qt.New(t)
220-
221-
c.Assert(DefaultContentTypes.IsContentFile(filepath.FromSlash("my/file.md")), qt.Equals, true)
222-
c.Assert(DefaultContentTypes.IsContentFile(filepath.FromSlash("my/file.ad")), qt.Equals, true)
223-
c.Assert(DefaultContentTypes.IsContentFile(filepath.FromSlash("textfile.txt")), qt.Equals, false)
224-
}

parser/lowercase_camel_json.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func (c ReplacingJSONMarshaller) MarshalJSON() ([]byte, error) {
107107
var removeZeroVAlues func(m map[string]any)
108108
removeZeroVAlues = func(m map[string]any) {
109109
for k, v := range m {
110-
if !hreflect.IsTruthful(v) {
110+
if !hreflect.IsMap(v) && !hreflect.IsTruthful(v) {
111111
delete(m, k)
112112
} else {
113113
switch vv := v.(type) {

resources/page/page_nop.go

+1-4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
"html/template"
2222
"time"
2323

24-
"github.com/gohugoio/hugo/hugofs/files"
2524
"github.com/gohugoio/hugo/markup/converter"
2625
"github.com/gohugoio/hugo/markup/tableofcontents"
2726

@@ -59,8 +58,6 @@ var (
5958
// PageNop implements Page, but does nothing.
6059
type nopPage int
6160

62-
var noOpPathInfo = media.DefaultPathParser.Parse(files.ComponentFolderContent, "no-op.md")
63-
6461
func (p *nopPage) Aliases() []string {
6562
return nil
6663
}
@@ -338,7 +335,7 @@ func (p *nopPage) Path() string {
338335
}
339336

340337
func (p *nopPage) PathInfo() *paths.Path {
341-
return noOpPathInfo
338+
return nil
342339
}
343340

344341
func (p *nopPage) Permalink() string {

source/fileInfo.go

+1
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ func (fi *File) p() *paths.Path {
132132
return fi.fim.Meta().PathInfo.Unnormalized()
133133
}
134134

135+
// Used in tests.
135136
func NewFileInfoFrom(path, filename string) *File {
136137
meta := &hugofs.FileMeta{
137138
Filename: filename,

0 commit comments

Comments
 (0)