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
This commit is contained in:
parent
4245a4514d
commit
c2fb221209
12 changed files with 182 additions and 52 deletions
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
115
media/config.go
115
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")
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue