aboutsummaryrefslogtreecommitdiffhomepage
path: root/media
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <[email protected]>2023-01-04 18:24:36 +0100
committerBjørn Erik Pedersen <[email protected]>2023-05-16 18:01:29 +0200
commit241b21b0fd34d91fccb2ce69874110dceae6f926 (patch)
treed4e0118eac7e9c42f065815447a70805f8d6ad3e /media
parent6aededf6b42011c3039f5f66487a89a8dd65e0e7 (diff)
downloadhugo-241b21b0fd34d91fccb2ce69874110dceae6f926.tar.gz
hugo-241b21b0fd34d91fccb2ce69874110dceae6f926.zip
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
Diffstat (limited to 'media')
-rw-r--r--media/builtin.go163
-rw-r--r--media/config.go139
-rw-r--r--media/config_test.go150
-rw-r--r--media/mediaType.go305
-rw-r--r--media/mediaType_test.go174
5 files changed, 541 insertions, 390 deletions
diff --git a/media/builtin.go b/media/builtin.go
new file mode 100644
index 000000000..64b5163b8
--- /dev/null
+++ b/media/builtin.go
@@ -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"] = "."
+ }
+}
diff --git a/media/config.go b/media/config.go
new file mode 100644
index 000000000..72583f267
--- /dev/null
+++ b/media/config.go
@@ -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.
+`)
+}
diff --git a/media/config_test.go b/media/config_test.go
new file mode 100644
index 000000000..75ede75bd
--- /dev/null
+++ b/media/config_test.go
@@ -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)
+}
diff --git a/media/mediaType.go b/media/mediaType.go
index 084f1fb5b..8204fc435 100644
--- a/media/mediaType.go
+++ b/media/mediaType.go
@@ -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, ","),
})
}
diff --git a/media/mediaType_test.go b/media/mediaType_test.go
index 4ddafc7c5..2e3a4a914 100644
--- a/media/mediaType_test.go
+++ b/media/mediaType_test.go
@@ -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")