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

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