Create a struct with all of Hugo's config options

Primary motivation is documentation, but it will also hopefully simplify the code.

Also,

* Lower case the default output format names; this is in line with the custom ones (map keys) and how
it's treated all the places. This avoids doing `stringds.EqualFold` everywhere.

Closes #10896
Closes #10620
This commit is contained in:
Bjørn Erik Pedersen
2023-01-04 18:24:36 +01:00
parent 6aededf6b4
commit 241b21b0fd
337 changed files with 13377 additions and 14898 deletions

163
media/builtin.go Normal file
View File

@@ -0,0 +1,163 @@
package media
type BuiltinTypes struct {
CalendarType Type
CSSType Type
SCSSType Type
SASSType Type
CSVType Type
HTMLType Type
JavascriptType Type
TypeScriptType Type
TSXType Type
JSXType Type
JSONType Type
WebAppManifestType Type
RSSType Type
XMLType Type
SVGType Type
TextType Type
TOMLType Type
YAMLType Type
// Common image types
PNGType Type
JPEGType Type
GIFType Type
TIFFType Type
BMPType Type
WEBPType Type
// Common font types
TrueTypeFontType Type
OpenTypeFontType Type
// Common document types
PDFType Type
MarkdownType Type
// Common video types
AVIType Type
MPEGType Type
MP4Type Type
OGGType Type
WEBMType Type
GPPType Type
// wasm
WasmType Type
OctetType Type
}
var (
Builtin = BuiltinTypes{
CalendarType: Type{Type: "text/calendar"},
CSSType: Type{Type: "text/css"},
SCSSType: Type{Type: "text/x-scss"},
SASSType: Type{Type: "text/x-sass"},
CSVType: Type{Type: "text/csv"},
HTMLType: Type{Type: "text/html"},
JavascriptType: Type{Type: "text/javascript"},
TypeScriptType: Type{Type: "text/typescript"},
TSXType: Type{Type: "text/tsx"},
JSXType: Type{Type: "text/jsx"},
JSONType: Type{Type: "application/json"},
WebAppManifestType: Type{Type: "application/manifest+json"},
RSSType: Type{Type: "application/rss+xml"},
XMLType: Type{Type: "application/xml"},
SVGType: Type{Type: "image/svg+xml"},
TextType: Type{Type: "text/plain"},
TOMLType: Type{Type: "application/toml"},
YAMLType: Type{Type: "application/yaml"},
// Common image types
PNGType: Type{Type: "image/png"},
JPEGType: Type{Type: "image/jpeg"},
GIFType: Type{Type: "image/gif"},
TIFFType: Type{Type: "image/tiff"},
BMPType: Type{Type: "image/bmp"},
WEBPType: Type{Type: "image/webp"},
// Common font types
TrueTypeFontType: Type{Type: "font/ttf"},
OpenTypeFontType: Type{Type: "font/otf"},
// Common document types
PDFType: Type{Type: "application/pdf"},
MarkdownType: Type{Type: "text/markdown"},
// Common video types
AVIType: Type{Type: "video/x-msvideo"},
MPEGType: Type{Type: "video/mpeg"},
MP4Type: Type{Type: "video/mp4"},
OGGType: Type{Type: "video/ogg"},
WEBMType: Type{Type: "video/webm"},
GPPType: Type{Type: "video/3gpp"},
// Web assembly.
WasmType: Type{Type: "application/wasm"},
OctetType: Type{Type: "application/octet-stream"},
}
)
var defaultMediaTypesConfig = map[string]any{
"text/calendar": map[string]any{"suffixes": []string{"ics"}},
"text/css": map[string]any{"suffixes": []string{"css"}},
"text/x-scss": map[string]any{"suffixes": []string{"scss"}},
"text/x-sass": map[string]any{"suffixes": []string{"sass"}},
"text/csv": map[string]any{"suffixes": []string{"csv"}},
"text/html": map[string]any{"suffixes": []string{"html"}},
"text/javascript": map[string]any{"suffixes": []string{"js", "jsm", "mjs"}},
"text/typescript": map[string]any{"suffixes": []string{"ts"}},
"text/tsx": map[string]any{"suffixes": []string{"tsx"}},
"text/jsx": map[string]any{"suffixes": []string{"jsx"}},
"application/json": map[string]any{"suffixes": []string{"json"}},
"application/manifest+json": map[string]any{"suffixes": []string{"webmanifest"}},
"application/rss+xml": map[string]any{"suffixes": []string{"xml", "rss"}},
"application/xml": map[string]any{"suffixes": []string{"xml"}},
"image/svg+xml": map[string]any{"suffixes": []string{"svg"}},
"text/plain": map[string]any{"suffixes": []string{"txt"}},
"application/toml": map[string]any{"suffixes": []string{"toml"}},
"application/yaml": map[string]any{"suffixes": []string{"yaml", "yml"}},
// Common image types
"image/png": map[string]any{"suffixes": []string{"png"}},
"image/jpeg": map[string]any{"suffixes": []string{"jpg", "jpeg", "jpe", "jif", "jfif"}},
"image/gif": map[string]any{"suffixes": []string{"gif"}},
"image/tiff": map[string]any{"suffixes": []string{"tif", "tiff"}},
"image/bmp": map[string]any{"suffixes": []string{"bmp"}},
"image/webp": map[string]any{"suffixes": []string{"webp"}},
// Common font types
"font/ttf": map[string]any{"suffixes": []string{"ttf"}},
"font/otf": map[string]any{"suffixes": []string{"otf"}},
// Common document types
"application/pdf": map[string]any{"suffixes": []string{"pdf"}},
"text/markdown": map[string]any{"suffixes": []string{"md", "markdown"}},
// Common video types
"video/x-msvideo": map[string]any{"suffixes": []string{"avi"}},
"video/mpeg": map[string]any{"suffixes": []string{"mpg", "mpeg"}},
"video/mp4": map[string]any{"suffixes": []string{"mp4"}},
"video/ogg": map[string]any{"suffixes": []string{"ogv"}},
"video/webm": map[string]any{"suffixes": []string{"webm"}},
"video/3gpp": map[string]any{"suffixes": []string{"3gpp", "3gp"}},
// wasm
"application/wasm": map[string]any{"suffixes": []string{"wasm"}},
"application/octet-stream": map[string]any{},
}
func init() {
// Apply delimiter to all.
for _, m := range defaultMediaTypesConfig {
m.(map[string]any)["delimiter"] = "."
}
}

139
media/config.go Normal file
View File

@@ -0,0 +1,139 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package media
import (
"errors"
"fmt"
"reflect"
"sort"
"strings"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
)
// DefaultTypes is the default media types supported by Hugo.
var DefaultTypes Types
func init() {
ns, err := DecodeTypes(nil)
if err != nil {
panic(err)
}
DefaultTypes = ns.Config
// Initialize the Builtin types with values from DefaultTypes.
v := reflect.ValueOf(&Builtin).Elem()
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
builtinType := f.Interface().(Type)
defaultType, found := DefaultTypes.GetByType(builtinType.Type)
if !found {
panic(errors.New("missing default type for builtin type: " + builtinType.Type))
}
f.Set(reflect.ValueOf(defaultType))
}
}
// Hold the configuration for a given media type.
type MediaTypeConfig struct {
// The file suffixes used for this media type.
Suffixes []string
// Delimiter used before suffix.
Delimiter string
}
// 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) {
m, err := maps.ToStringMapE(v)
if err != nil {
return nil, nil, err
}
if m == nil {
m = map[string]any{}
}
m = maps.CleanConfigStringMap(m)
// Merge with defaults.
maps.MergeShallow(m, defaultMediaTypesConfig)
var types Types
for k, v := range m {
mediaType, err := FromString(k)
if err != nil {
return nil, nil, err
}
if err := mapstructure.WeakDecode(v, &mediaType); err != nil {
return nil, nil, err
}
mm := maps.ToStringMap(v)
suffixes, found := maps.LookupEqualFold(mm, "suffixes")
if found {
mediaType.SuffixesCSV = strings.TrimSpace(strings.ToLower(strings.Join(cast.ToStringSlice(suffixes), ",")))
}
if mediaType.SuffixesCSV != "" && mediaType.Delimiter == "" {
mediaType.Delimiter = DefaultDelimiter
}
InitMediaType(&mediaType)
types = append(types, mediaType)
}
sort.Sort(types)
return types, m, nil
}
ns, err := config.DecodeNamespace[map[string]MediaTypeConfig](in, buildConfig)
if err != nil {
return nil, fmt.Errorf("failed to decode media types: %w", err)
}
return ns, nil
}
func suffixIsRemoved() error {
return errors.New(`MediaType.Suffix is removed. Before Hugo 0.44 this was used both to set a custom file suffix and as way
to augment the mediatype definition (what you see after the "+", e.g. "image/svg+xml").
This had its limitations. For one, it was only possible with one file extension per MIME type.
Now you can specify multiple file suffixes using "suffixes", but you need to specify the full MIME type
identifier:
[mediaTypes]
[mediaTypes."image/svg+xml"]
suffixes = ["svg", "abc" ]
In most cases, it will be enough to just change:
[mediaTypes]
[mediaTypes."my/custom-mediatype"]
suffix = "txt"
To:
[mediaTypes]
[mediaTypes."my/custom-mediatype"]
suffixes = ["txt"]
Note that you can still get the Media Type's suffix from a template: {{ $mediaType.Suffix }}. But this will now map to the MIME type filename.
`)
}

150
media/config_test.go Normal file
View File

@@ -0,0 +1,150 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package media
import (
"fmt"
"testing"
qt "github.com/frankban/quicktest"
)
func TestDecodeTypes(t *testing.T) {
c := qt.New(t)
tests := []struct {
name string
m map[string]any
shouldError bool
assert func(t *testing.T, name string, tt Types)
}{
{
"Redefine JSON",
map[string]any{
"application/json": map[string]any{
"suffixes": []string{"jasn"},
},
},
false,
func(t *testing.T, name string, tt Types) {
for _, ttt := range tt {
if _, ok := DefaultTypes.GetByType(ttt.Type); !ok {
fmt.Println(ttt.Type, "not found in default types")
}
}
c.Assert(len(tt), qt.Equals, len(DefaultTypes))
json, si, found := tt.GetBySuffix("jasn")
c.Assert(found, qt.Equals, true)
c.Assert(json.String(), qt.Equals, "application/json")
c.Assert(si.FullSuffix, qt.Equals, ".jasn")
},
},
{
"MIME suffix in key, multiple file suffixes, custom delimiter",
map[string]any{
"application/hugo+hg": map[string]any{
"suffixes": []string{"hg1", "hG2"},
"Delimiter": "_",
},
},
false,
func(t *testing.T, name string, tt Types) {
c.Assert(len(tt), qt.Equals, len(DefaultTypes)+1)
hg, si, found := tt.GetBySuffix("hg2")
c.Assert(found, qt.Equals, true)
c.Assert(hg.FirstSuffix.Suffix, qt.Equals, "hg1")
c.Assert(hg.FirstSuffix.FullSuffix, qt.Equals, "_hg1")
c.Assert(si.Suffix, qt.Equals, "hg2")
c.Assert(si.FullSuffix, qt.Equals, "_hg2")
c.Assert(hg.String(), qt.Equals, "application/hugo+hg")
_, found = tt.GetByType("application/hugo+hg")
c.Assert(found, qt.Equals, true)
},
},
{
"Add custom media type",
map[string]any{
"text/hugo+hgo": map[string]any{
"Suffixes": []string{"hgo2"},
},
},
false,
func(t *testing.T, name string, tp Types) {
c.Assert(len(tp), qt.Equals, len(DefaultTypes)+1)
// Make sure we have not broken the default config.
_, _, found := tp.GetBySuffix("json")
c.Assert(found, qt.Equals, true)
hugo, _, found := tp.GetBySuffix("hgo2")
c.Assert(found, qt.Equals, true)
c.Assert(hugo.String(), qt.Equals, "text/hugo+hgo")
},
},
}
for _, test := range tests {
result, err := DecodeTypes(test.m)
if test.shouldError {
c.Assert(err, qt.Not(qt.IsNil))
} else {
c.Assert(err, qt.IsNil)
test.assert(t, test.name, result.Config)
}
}
}
func TestDefaultTypes(t *testing.T) {
c := qt.New(t)
for _, test := range []struct {
tp Type
expectedMainType string
expectedSubType string
expectedSuffix string
expectedType string
expectedString string
}{
{Builtin.CalendarType, "text", "calendar", "ics", "text/calendar", "text/calendar"},
{Builtin.CSSType, "text", "css", "css", "text/css", "text/css"},
{Builtin.SCSSType, "text", "x-scss", "scss", "text/x-scss", "text/x-scss"},
{Builtin.CSVType, "text", "csv", "csv", "text/csv", "text/csv"},
{Builtin.HTMLType, "text", "html", "html", "text/html", "text/html"},
{Builtin.JavascriptType, "text", "javascript", "js", "text/javascript", "text/javascript"},
{Builtin.TypeScriptType, "text", "typescript", "ts", "text/typescript", "text/typescript"},
{Builtin.TSXType, "text", "tsx", "tsx", "text/tsx", "text/tsx"},
{Builtin.JSXType, "text", "jsx", "jsx", "text/jsx", "text/jsx"},
{Builtin.JSONType, "application", "json", "json", "application/json", "application/json"},
{Builtin.RSSType, "application", "rss", "xml", "application/rss+xml", "application/rss+xml"},
{Builtin.SVGType, "image", "svg", "svg", "image/svg+xml", "image/svg+xml"},
{Builtin.TextType, "text", "plain", "txt", "text/plain", "text/plain"},
{Builtin.XMLType, "application", "xml", "xml", "application/xml", "application/xml"},
{Builtin.TOMLType, "application", "toml", "toml", "application/toml", "application/toml"},
{Builtin.YAMLType, "application", "yaml", "yaml", "application/yaml", "application/yaml"},
{Builtin.PDFType, "application", "pdf", "pdf", "application/pdf", "application/pdf"},
{Builtin.TrueTypeFontType, "font", "ttf", "ttf", "font/ttf", "font/ttf"},
{Builtin.OpenTypeFontType, "font", "otf", "otf", "font/otf", "font/otf"},
} {
c.Assert(test.tp.MainType, qt.Equals, test.expectedMainType)
c.Assert(test.tp.SubType, qt.Equals, test.expectedSubType)
c.Assert(test.tp.Type, qt.Equals, test.expectedType)
c.Assert(test.tp.String(), qt.Equals, test.expectedString)
}
c.Assert(len(DefaultTypes), qt.Equals, 36)
}

View File

@@ -16,38 +16,36 @@ package media
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
"strings"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/common/maps"
"github.com/mitchellh/mapstructure"
)
var zero Type
const (
defaultDelimiter = "."
DefaultDelimiter = "."
)
// Type (also known as MIME type and content type) is a two-part identifier for
// MediaType (also known as MIME type and content type) is a two-part identifier for
// file formats and format contents transmitted on the Internet.
// For Hugo's use case, we use the top-level type name / subtype name + suffix.
// One example would be application/svg+xml
// If suffix is not provided, the sub type will be used.
// See // https://en.wikipedia.org/wiki/Media_type
// <docsmeta>{ "name": "MediaType" }</docsmeta>
type Type struct {
MainType string `json:"mainType"` // i.e. text
SubType string `json:"subType"` // i.e. html
Delimiter string `json:"delimiter"` // e.g. "."
// The full MIME type string, e.g. "application/rss+xml".
Type string `json:"-"`
// FirstSuffix holds the first suffix defined for this Type.
FirstSuffix SuffixInfo `json:"firstSuffix"`
// The top-level type name, e.g. "application".
MainType string `json:"mainType"`
// The subtype name, e.g. "rss".
SubType string `json:"subType"`
// The delimiter before the suffix, e.g. ".".
Delimiter string `json:"delimiter"`
// FirstSuffix holds the first suffix defined for this MediaType.
FirstSuffix SuffixInfo `json:"-"`
// This is the optional suffix after the "+" in the MIME type,
// e.g. "xml" in "application/rss+xml".
@@ -55,12 +53,16 @@ type Type struct {
// E.g. "jpg,jpeg"
// Stored as a string to make Type comparable.
suffixesCSV string
// For internal use only.
SuffixesCSV string `json:"-"`
}
// SuffixInfo holds information about a Type's suffix.
// SuffixInfo holds information about a Media Type's suffix.
type SuffixInfo struct {
Suffix string `json:"suffix"`
// Suffix is the suffix without the delimiter, e.g. "xml".
Suffix string `json:"suffix"`
// FullSuffix is the suffix with the delimiter, e.g. ".xml".
FullSuffix string `json:"fullSuffix"`
}
@@ -121,12 +123,21 @@ func FromStringAndExt(t, ext string) (Type, error) {
if err != nil {
return tp, err
}
tp.suffixesCSV = strings.TrimPrefix(ext, ".")
tp.Delimiter = defaultDelimiter
tp.SuffixesCSV = strings.TrimPrefix(ext, ".")
tp.Delimiter = DefaultDelimiter
tp.init()
return tp, nil
}
// MustFromString is like FromString but panics on error.
func MustFromString(t string) Type {
tp, err := FromString(t)
if err != nil {
panic(err)
}
return tp
}
// FromString creates a new Type given a type string on the form MainType/SubType and
// an optional suffix, e.g. "text/html" or "text/html+html".
func FromString(t string) (Type, error) {
@@ -146,52 +157,49 @@ func FromString(t string) (Type, error) {
suffix = subParts[1]
}
return Type{MainType: mainType, SubType: subType, mimeSuffix: suffix}, nil
}
// Type returns a string representing the main- and sub-type of a media type, e.g. "text/css".
// A suffix identifier will be appended after a "+" if set, e.g. "image/svg+xml".
// Hugo will register a set of default media types.
// These can be overridden by the user in the configuration,
// by defining a media type with the same Type.
func (m Type) Type() string {
// Examples are
// image/svg+xml
// text/css
if m.mimeSuffix != "" {
return m.MainType + "/" + m.SubType + "+" + m.mimeSuffix
var typ string
if suffix != "" {
typ = mainType + "/" + subType + "+" + suffix
} else {
typ = mainType + "/" + subType
}
return m.MainType + "/" + m.SubType
return Type{Type: typ, MainType: mainType, SubType: subType, mimeSuffix: suffix}, nil
}
// For internal use.
func (m Type) String() string {
return m.Type()
return m.Type
}
// Suffixes returns all valid file suffixes for this type.
func (m Type) Suffixes() []string {
if m.suffixesCSV == "" {
if m.SuffixesCSV == "" {
return nil
}
return strings.Split(m.suffixesCSV, ",")
return strings.Split(m.SuffixesCSV, ",")
}
// IsText returns whether this Type is a text format.
// Note that this may currently return false negatives.
// TODO(bep) improve
// For internal use.
func (m Type) IsText() bool {
if m.MainType == "text" {
return true
}
switch m.SubType {
case "javascript", "json", "rss", "xml", "svg", TOMLType.SubType, YAMLType.SubType:
case "javascript", "json", "rss", "xml", "svg", "toml", "yml", "yaml":
return true
}
return false
}
func InitMediaType(m *Type) {
m.init()
}
func (m *Type) init() {
m.FirstSuffix.FullSuffix = ""
m.FirstSuffix.Suffix = ""
@@ -204,13 +212,13 @@ func (m *Type) init() {
// WithDelimiterAndSuffixes is used in tests.
func WithDelimiterAndSuffixes(t Type, delimiter, suffixesCSV string) Type {
t.Delimiter = delimiter
t.suffixesCSV = suffixesCSV
t.SuffixesCSV = suffixesCSV
t.init()
return t
}
func newMediaType(main, sub string, suffixes []string) Type {
t := Type{MainType: main, SubType: sub, suffixesCSV: strings.Join(suffixes, ","), Delimiter: defaultDelimiter}
t := Type{MainType: main, SubType: sub, SuffixesCSV: strings.Join(suffixes, ","), Delimiter: DefaultDelimiter}
t.init()
return t
}
@@ -222,118 +230,18 @@ func newMediaTypeWithMimeSuffix(main, sub, mimeSuffix string, suffixes []string)
return mt
}
// Definitions from https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types etc.
// Note that from Hugo 0.44 we only set Suffix if it is part of the MIME type.
var (
CalendarType = newMediaType("text", "calendar", []string{"ics"})
CSSType = newMediaType("text", "css", []string{"css"})
SCSSType = newMediaType("text", "x-scss", []string{"scss"})
SASSType = newMediaType("text", "x-sass", []string{"sass"})
CSVType = newMediaType("text", "csv", []string{"csv"})
HTMLType = newMediaType("text", "html", []string{"html"})
JavascriptType = newMediaType("text", "javascript", []string{"js", "jsm", "mjs"})
TypeScriptType = newMediaType("text", "typescript", []string{"ts"})
TSXType = newMediaType("text", "tsx", []string{"tsx"})
JSXType = newMediaType("text", "jsx", []string{"jsx"})
JSONType = newMediaType("application", "json", []string{"json"})
WebAppManifestType = newMediaTypeWithMimeSuffix("application", "manifest", "json", []string{"webmanifest"})
RSSType = newMediaTypeWithMimeSuffix("application", "rss", "xml", []string{"xml", "rss"})
XMLType = newMediaType("application", "xml", []string{"xml"})
SVGType = newMediaTypeWithMimeSuffix("image", "svg", "xml", []string{"svg"})
TextType = newMediaType("text", "plain", []string{"txt"})
TOMLType = newMediaType("application", "toml", []string{"toml"})
YAMLType = newMediaType("application", "yaml", []string{"yaml", "yml"})
// Common image types
PNGType = newMediaType("image", "png", []string{"png"})
JPEGType = newMediaType("image", "jpeg", []string{"jpg", "jpeg", "jpe", "jif", "jfif"})
GIFType = newMediaType("image", "gif", []string{"gif"})
TIFFType = newMediaType("image", "tiff", []string{"tif", "tiff"})
BMPType = newMediaType("image", "bmp", []string{"bmp"})
WEBPType = newMediaType("image", "webp", []string{"webp"})
// Common font types
TrueTypeFontType = newMediaType("font", "ttf", []string{"ttf"})
OpenTypeFontType = newMediaType("font", "otf", []string{"otf"})
// Common document types
PDFType = newMediaType("application", "pdf", []string{"pdf"})
MarkdownType = newMediaType("text", "markdown", []string{"md", "markdown"})
// Common video types
AVIType = newMediaType("video", "x-msvideo", []string{"avi"})
MPEGType = newMediaType("video", "mpeg", []string{"mpg", "mpeg"})
MP4Type = newMediaType("video", "mp4", []string{"mp4"})
OGGType = newMediaType("video", "ogg", []string{"ogv"})
WEBMType = newMediaType("video", "webm", []string{"webm"})
GPPType = newMediaType("video", "3gpp", []string{"3gpp", "3gp"})
OctetType = newMediaType("application", "octet-stream", nil)
)
// DefaultTypes is the default media types supported by Hugo.
var DefaultTypes = Types{
CalendarType,
CSSType,
CSVType,
SCSSType,
SASSType,
HTMLType,
MarkdownType,
JavascriptType,
TypeScriptType,
TSXType,
JSXType,
JSONType,
WebAppManifestType,
RSSType,
XMLType,
SVGType,
TextType,
OctetType,
YAMLType,
TOMLType,
PNGType,
GIFType,
BMPType,
JPEGType,
WEBPType,
AVIType,
MPEGType,
MP4Type,
OGGType,
WEBMType,
GPPType,
OpenTypeFontType,
TrueTypeFontType,
PDFType,
}
func init() {
sort.Sort(DefaultTypes)
// Sanity check.
seen := make(map[Type]bool)
for _, t := range DefaultTypes {
if seen[t] {
panic(fmt.Sprintf("MediaType %s duplicated in list", t))
}
seen[t] = true
}
}
// Types is a slice of media types.
// <docsmeta>{ "name": "MediaTypes" }</docsmeta>
type Types []Type
func (t Types) Len() int { return len(t) }
func (t Types) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t Types) Less(i, j int) bool { return t[i].Type() < t[j].Type() }
func (t Types) Less(i, j int) bool { return t[i].Type < t[j].Type }
// GetByType returns a media type for tp.
func (t Types) GetByType(tp string) (Type, bool) {
for _, tt := range t {
if strings.EqualFold(tt.Type(), tp) {
if strings.EqualFold(tt.Type, tp) {
return tt, true
}
}
@@ -399,8 +307,19 @@ func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
return
}
func (t Types) IsTextSuffix(suffix string) bool {
suffix = strings.ToLower(suffix)
for _, tt := range t {
if tt.hasSuffix(suffix) {
return tt.IsText()
}
}
return false
}
func (m Type) hasSuffix(suffix string) bool {
return strings.Contains(","+m.suffixesCSV+",", ","+suffix+",")
return strings.Contains(","+m.SuffixesCSV+",", ","+suffix+",")
}
// GetByMainSubType gets a media type given a main and a sub type e.g. "text" and "plain".
@@ -423,96 +342,6 @@ func (t Types) GetByMainSubType(mainType, subType string) (tp Type, found bool)
return
}
func suffixIsRemoved() error {
return errors.New(`MediaType.Suffix is removed. Before Hugo 0.44 this was used both to set a custom file suffix and as way
to augment the mediatype definition (what you see after the "+", e.g. "image/svg+xml").
This had its limitations. For one, it was only possible with one file extension per MIME type.
Now you can specify multiple file suffixes using "suffixes", but you need to specify the full MIME type
identifier:
[mediaTypes]
[mediaTypes."image/svg+xml"]
suffixes = ["svg", "abc" ]
In most cases, it will be enough to just change:
[mediaTypes]
[mediaTypes."my/custom-mediatype"]
suffix = "txt"
To:
[mediaTypes]
[mediaTypes."my/custom-mediatype"]
suffixes = ["txt"]
Note that you can still get the Media Type's suffix from a template: {{ $mediaType.Suffix }}. But this will now map to the MIME type filename.
`)
}
// DecodeTypes takes a list of media type configurations and merges those,
// in the order given, with the Hugo defaults as the last resort.
func DecodeTypes(mms ...map[string]any) (Types, error) {
var m Types
// Maps type string to Type. Type string is the full application/svg+xml.
mmm := make(map[string]Type)
for _, dt := range DefaultTypes {
mmm[dt.Type()] = dt
}
for _, mm := range mms {
for k, v := range mm {
var mediaType Type
mediaType, found := mmm[k]
if !found {
var err error
mediaType, err = FromString(k)
if err != nil {
return m, err
}
}
if err := mapstructure.WeakDecode(v, &mediaType); err != nil {
return m, err
}
vm := maps.ToStringMap(v)
maps.PrepareParams(vm)
_, delimiterSet := vm["delimiter"]
_, suffixSet := vm["suffix"]
if suffixSet {
return Types{}, suffixIsRemoved()
}
if suffixes, found := vm["suffixes"]; found {
mediaType.suffixesCSV = strings.TrimSpace(strings.ToLower(strings.Join(cast.ToStringSlice(suffixes), ",")))
}
// The user may set the delimiter as an empty string.
if !delimiterSet && mediaType.suffixesCSV != "" {
mediaType.Delimiter = defaultDelimiter
}
mediaType.init()
mmm[k] = mediaType
}
}
for _, v := range mmm {
m = append(m, v)
}
sort.Sort(m)
return m, nil
}
// IsZero reports whether this Type represents a zero value.
// For internal use.
func (m Type) IsZero() bool {
@@ -530,8 +359,8 @@ func (m Type) MarshalJSON() ([]byte, error) {
Suffixes []string `json:"suffixes"`
}{
Alias: (Alias)(m),
Type: m.Type(),
Type: m.Type,
String: m.String(),
Suffixes: strings.Split(m.suffixesCSV, ","),
Suffixes: strings.Split(m.SuffixesCSV, ","),
})
}

View File

@@ -25,73 +25,32 @@ import (
"github.com/gohugoio/hugo/common/paths"
)
func TestDefaultTypes(t *testing.T) {
c := qt.New(t)
for _, test := range []struct {
tp Type
expectedMainType string
expectedSubType string
expectedSuffix string
expectedType string
expectedString string
}{
{CalendarType, "text", "calendar", "ics", "text/calendar", "text/calendar"},
{CSSType, "text", "css", "css", "text/css", "text/css"},
{SCSSType, "text", "x-scss", "scss", "text/x-scss", "text/x-scss"},
{CSVType, "text", "csv", "csv", "text/csv", "text/csv"},
{HTMLType, "text", "html", "html", "text/html", "text/html"},
{JavascriptType, "text", "javascript", "js", "text/javascript", "text/javascript"},
{TypeScriptType, "text", "typescript", "ts", "text/typescript", "text/typescript"},
{TSXType, "text", "tsx", "tsx", "text/tsx", "text/tsx"},
{JSXType, "text", "jsx", "jsx", "text/jsx", "text/jsx"},
{JSONType, "application", "json", "json", "application/json", "application/json"},
{RSSType, "application", "rss", "xml", "application/rss+xml", "application/rss+xml"},
{SVGType, "image", "svg", "svg", "image/svg+xml", "image/svg+xml"},
{TextType, "text", "plain", "txt", "text/plain", "text/plain"},
{XMLType, "application", "xml", "xml", "application/xml", "application/xml"},
{TOMLType, "application", "toml", "toml", "application/toml", "application/toml"},
{YAMLType, "application", "yaml", "yaml", "application/yaml", "application/yaml"},
{PDFType, "application", "pdf", "pdf", "application/pdf", "application/pdf"},
{TrueTypeFontType, "font", "ttf", "ttf", "font/ttf", "font/ttf"},
{OpenTypeFontType, "font", "otf", "otf", "font/otf", "font/otf"},
} {
c.Assert(test.tp.MainType, qt.Equals, test.expectedMainType)
c.Assert(test.tp.SubType, qt.Equals, test.expectedSubType)
c.Assert(test.tp.Type(), qt.Equals, test.expectedType)
c.Assert(test.tp.String(), qt.Equals, test.expectedString)
}
c.Assert(len(DefaultTypes), qt.Equals, 34)
}
func TestGetByType(t *testing.T) {
c := qt.New(t)
types := Types{HTMLType, RSSType}
types := DefaultTypes
mt, found := types.GetByType("text/HTML")
c.Assert(found, qt.Equals, true)
c.Assert(HTMLType, qt.Equals, mt)
c.Assert(mt.SubType, qt.Equals, "html")
_, found = types.GetByType("text/nono")
c.Assert(found, qt.Equals, false)
mt, found = types.GetByType("application/rss+xml")
c.Assert(found, qt.Equals, true)
c.Assert(RSSType, qt.Equals, mt)
c.Assert(mt.SubType, qt.Equals, "rss")
mt, found = types.GetByType("application/rss")
c.Assert(found, qt.Equals, true)
c.Assert(RSSType, qt.Equals, mt)
c.Assert(mt.SubType, qt.Equals, "rss")
}
func TestGetByMainSubType(t *testing.T) {
c := qt.New(t)
f, found := DefaultTypes.GetByMainSubType("text", "plain")
c.Assert(found, qt.Equals, true)
c.Assert(f, qt.Equals, TextType)
c.Assert(f.SubType, qt.Equals, "plain")
_, found = DefaultTypes.GetByMainSubType("foo", "plain")
c.Assert(found, qt.Equals, false)
}
@@ -107,7 +66,8 @@ func TestBySuffix(t *testing.T) {
func TestGetFirstBySuffix(t *testing.T) {
c := qt.New(t)
types := DefaultTypes
types := make(Types, len(DefaultTypes))
copy(types, DefaultTypes)
// Issue #8406
geoJSON := newMediaTypeWithMimeSuffix("application", "geo", "json", []string{"geojson", "gjson"})
@@ -124,8 +84,8 @@ func TestGetFirstBySuffix(t *testing.T) {
c.Assert(t, qt.Equals, expectedType)
}
check("js", JavascriptType)
check("json", JSONType)
check("js", Builtin.JavascriptType)
check("json", Builtin.JSONType)
check("geojson", geoJSON)
check("gjson", geoJSON)
}
@@ -134,15 +94,15 @@ func TestFromTypeString(t *testing.T) {
c := qt.New(t)
f, err := FromString("text/html")
c.Assert(err, qt.IsNil)
c.Assert(f.Type(), qt.Equals, HTMLType.Type())
c.Assert(f.Type, qt.Equals, Builtin.HTMLType.Type)
f, err = FromString("application/custom")
c.Assert(err, qt.IsNil)
c.Assert(f, qt.Equals, Type{MainType: "application", SubType: "custom", mimeSuffix: ""})
c.Assert(f, qt.Equals, Type{Type: "application/custom", MainType: "application", SubType: "custom", mimeSuffix: ""})
f, err = FromString("application/custom+sfx")
c.Assert(err, qt.IsNil)
c.Assert(f, qt.Equals, Type{MainType: "application", SubType: "custom", mimeSuffix: "sfx"})
c.Assert(f, qt.Equals, Type{Type: "application/custom+sfx", MainType: "application", SubType: "custom", mimeSuffix: "sfx"})
_, err = FromString("noslash")
c.Assert(err, qt.Not(qt.IsNil))
@@ -150,17 +110,17 @@ func TestFromTypeString(t *testing.T) {
f, err = FromString("text/xml; charset=utf-8")
c.Assert(err, qt.IsNil)
c.Assert(f, qt.Equals, Type{MainType: "text", SubType: "xml", mimeSuffix: ""})
c.Assert(f, qt.Equals, Type{Type: "text/xml", MainType: "text", SubType: "xml", mimeSuffix: ""})
}
func TestFromStringAndExt(t *testing.T) {
c := qt.New(t)
f, err := FromStringAndExt("text/html", "html")
c.Assert(err, qt.IsNil)
c.Assert(f, qt.Equals, HTMLType)
c.Assert(f, qt.Equals, Builtin.HTMLType)
f, err = FromStringAndExt("text/html", ".html")
c.Assert(err, qt.IsNil)
c.Assert(f, qt.Equals, HTMLType)
c.Assert(f, qt.Equals, Builtin.HTMLType)
}
// Add a test for the SVG case
@@ -185,7 +145,6 @@ func TestFromContent(t *testing.T) {
files, err := filepath.Glob("./testdata/resource.*")
c.Assert(err, qt.IsNil)
mtypes := DefaultTypes
for _, filename := range files {
name := filepath.Base(filename)
@@ -199,9 +158,9 @@ func TestFromContent(t *testing.T) {
} else {
exts = []string{ext}
}
expected, _, found := mtypes.GetFirstBySuffix(ext)
expected, _, found := DefaultTypes.GetFirstBySuffix(ext)
c.Assert(found, qt.IsTrue)
got := FromContent(mtypes, exts, content)
got := FromContent(DefaultTypes, exts, content)
c.Assert(got, qt.Equals, expected)
})
}
@@ -212,7 +171,6 @@ func TestFromContentFakes(t *testing.T) {
files, err := filepath.Glob("./testdata/fake.*")
c.Assert(err, qt.IsNil)
mtypes := DefaultTypes
for _, filename := range files {
name := filepath.Base(filename)
@@ -220,109 +178,21 @@ func TestFromContentFakes(t *testing.T) {
content, err := os.ReadFile(filename)
c.Assert(err, qt.IsNil)
ext := strings.TrimPrefix(paths.Ext(filename), ".")
got := FromContent(mtypes, []string{ext}, content)
got := FromContent(DefaultTypes, []string{ext}, content)
c.Assert(got, qt.Equals, zero)
})
}
}
func TestDecodeTypes(t *testing.T) {
c := qt.New(t)
tests := []struct {
name string
maps []map[string]any
shouldError bool
assert func(t *testing.T, name string, tt Types)
}{
{
"Redefine JSON",
[]map[string]any{
{
"application/json": map[string]any{
"suffixes": []string{"jasn"},
},
},
},
false,
func(t *testing.T, name string, tt Types) {
c.Assert(len(tt), qt.Equals, len(DefaultTypes))
json, si, found := tt.GetBySuffix("jasn")
c.Assert(found, qt.Equals, true)
c.Assert(json.String(), qt.Equals, "application/json")
c.Assert(si.FullSuffix, qt.Equals, ".jasn")
},
},
{
"MIME suffix in key, multiple file suffixes, custom delimiter",
[]map[string]any{
{
"application/hugo+hg": map[string]any{
"suffixes": []string{"hg1", "hG2"},
"Delimiter": "_",
},
},
},
false,
func(t *testing.T, name string, tt Types) {
c.Assert(len(tt), qt.Equals, len(DefaultTypes)+1)
hg, si, found := tt.GetBySuffix("hg2")
c.Assert(found, qt.Equals, true)
c.Assert(hg.mimeSuffix, qt.Equals, "hg")
c.Assert(hg.FirstSuffix.Suffix, qt.Equals, "hg1")
c.Assert(hg.FirstSuffix.FullSuffix, qt.Equals, "_hg1")
c.Assert(si.Suffix, qt.Equals, "hg2")
c.Assert(si.FullSuffix, qt.Equals, "_hg2")
c.Assert(hg.String(), qt.Equals, "application/hugo+hg")
_, found = tt.GetByType("application/hugo+hg")
c.Assert(found, qt.Equals, true)
},
},
{
"Add custom media type",
[]map[string]any{
{
"text/hugo+hgo": map[string]any{
"Suffixes": []string{"hgo2"},
},
},
},
false,
func(t *testing.T, name string, tp Types) {
c.Assert(len(tp), qt.Equals, len(DefaultTypes)+1)
// Make sure we have not broken the default config.
_, _, found := tp.GetBySuffix("json")
c.Assert(found, qt.Equals, true)
hugo, _, found := tp.GetBySuffix("hgo2")
c.Assert(found, qt.Equals, true)
c.Assert(hugo.String(), qt.Equals, "text/hugo+hgo")
},
},
}
for _, test := range tests {
result, err := DecodeTypes(test.maps...)
if test.shouldError {
c.Assert(err, qt.Not(qt.IsNil))
} else {
c.Assert(err, qt.IsNil)
test.assert(t, test.name, result)
}
}
}
func TestToJSON(t *testing.T) {
c := qt.New(t)
b, err := json.Marshal(MPEGType)
b, err := json.Marshal(Builtin.MPEGType)
c.Assert(err, qt.IsNil)
c.Assert(string(b), qt.Equals, `{"mainType":"video","subType":"mpeg","delimiter":".","firstSuffix":{"suffix":"mpg","fullSuffix":".mpg"},"type":"video/mpeg","string":"video/mpeg","suffixes":["mpg","mpeg"]}`)
c.Assert(string(b), qt.Equals, `{"mainType":"video","subType":"mpeg","delimiter":".","type":"video/mpeg","string":"video/mpeg","suffixes":["mpg","mpeg"]}`)
}
func BenchmarkTypeOps(b *testing.B) {
mt := MPEGType
mt := Builtin.MPEGType
mts := DefaultTypes
for i := 0; i < b.N; i++ {
ff := mt.FirstSuffix
@@ -335,7 +205,7 @@ func BenchmarkTypeOps(b *testing.B) {
_ = mt.String()
_ = ff.Suffix
_ = mt.Suffixes
_ = mt.Type()
_ = mt.Type
_ = mts.BySuffix("xml")
_, _ = mts.GetByMainSubType("application", "xml")
_, _, _ = mts.GetBySuffix("xml")