diff options
author | Bjørn Erik Pedersen <[email protected]> | 2023-01-04 18:24:36 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2023-05-16 18:01:29 +0200 |
commit | 241b21b0fd34d91fccb2ce69874110dceae6f926 (patch) | |
tree | d4e0118eac7e9c42f065815447a70805f8d6ad3e /media | |
parent | 6aededf6b42011c3039f5f66487a89a8dd65e0e7 (diff) | |
download | hugo-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.go | 163 | ||||
-rw-r--r-- | media/config.go | 139 | ||||
-rw-r--r-- | media/config_test.go | 150 | ||||
-rw-r--r-- | media/mediaType.go | 305 | ||||
-rw-r--r-- | media/mediaType_test.go | 174 |
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") |