diff --git a/deploy/deploy.go b/deploy/deploy.go index 1aaae5aa7..c75461750 100644 --- a/deploy/deploy.go +++ b/deploy/deploy.go @@ -432,7 +432,7 @@ func (lf *localFile) ContentType() string { } ext := filepath.Ext(lf.NativePath) - if mimeType, found := lf.mediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, ".")); found { + if mimeType, _, found := lf.mediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, ".")); found { return mimeType.Type() } diff --git a/hugolib/config_test.go b/hugolib/config_test.go index aeeee5fa5..ecb450067 100644 --- a/hugolib/config_test.go +++ b/hugolib/config_test.go @@ -220,8 +220,9 @@ map[string]interface {}{ MainType: "text", SubType: "m1", Delimiter: ".", - Suffixes: []string{ - "m1main", + FirstSuffix: SuffixInfo{ + Suffix: "m1main", + FullSuffix: ".m1main", }, }, }, @@ -231,8 +232,9 @@ map[string]interface {}{ MainType: "text", SubType: "m2", Delimiter: ".", - Suffixes: []string{ - "m2theme", + FirstSuffix: SuffixInfo{ + Suffix: "m2theme", + FullSuffix: ".m2theme", }, }, }, diff --git a/hugolib/pagebundler_test.go b/hugolib/pagebundler_test.go index fc02d5857..7d775871a 100644 --- a/hugolib/pagebundler_test.go +++ b/hugolib/pagebundler_test.go @@ -34,8 +34,6 @@ import ( "github.com/gohugoio/hugo/htesting" - "github.com/gohugoio/hugo/media" - "github.com/gohugoio/hugo/deps" "github.com/spf13/viper" @@ -76,7 +74,7 @@ func TestPageBundlerSiteRegular(t *testing.T) { cfg.Set("outputFormats", map[string]interface{}{ "CUSTOMO": map[string]interface{}{ - "mediaType": media.HTMLType, + "mediaType": "text/html", "baseName": "cindex", "path": "cpath", "permalinkable": true, diff --git a/hugolib/site.go b/hugolib/site.go index d2a5e68ae..3c7c03bd1 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -913,7 +913,7 @@ type whatChanged struct { // package, so it will behave correctly with Hugo's built-in server. func (s *Site) RegisterMediaTypes() { for _, mt := range s.mediaTypesConfig { - for _, suffix := range mt.Suffixes { + for _, suffix := range mt.Suffixes() { _ = mime.AddExtensionType(mt.Delimiter+suffix, mt.Type()+"; charset=utf-8") } } diff --git a/hugolib/site_output_test.go b/hugolib/site_output_test.go index 5a329942c..1961dd06f 100644 --- a/hugolib/site_output_test.go +++ b/hugolib/site_output_test.go @@ -305,13 +305,14 @@ baseName = "customdelimbase" c.Assert(err, qt.IsNil) + s := h.Sites[0] + th.assertFileContent("public/_redirects", "a dotless") th.assertFileContent("public/defaultdelimbase.defd", "default delimim") // This looks weird, but the user has chosen this definition. th.assertFileContent("public/nosuffixbase", "no suffix") th.assertFileContent("public/customdelimbase_del", "custom delim") - s := h.Sites[0] home := s.getPage(page.KindHome) c.Assert(home, qt.Not(qt.IsNil)) diff --git a/media/mediaType.go b/media/mediaType.go index 9e35212b2..a35d80e3e 100644 --- a/media/mediaType.go +++ b/media/mediaType.go @@ -20,6 +20,8 @@ import ( "sort" "strings" + "github.com/spf13/cast" + "github.com/gohugoio/hugo/common/maps" "github.com/mitchellh/mapstructure" @@ -36,28 +38,37 @@ const ( // If suffix is not provided, the sub type will be used. // See // https://en.wikipedia.org/wiki/Media_type type Type struct { - MainType string `json:"mainType"` // i.e. text - SubType string `json:"subType"` // i.e. html + MainType string `json:"mainType"` // i.e. text + SubType string `json:"subType"` // i.e. html + Delimiter string `json:"delimiter"` // e.g. "." + + // FirstSuffix holds the first suffix defined for this Type. + FirstSuffix SuffixInfo `json:"firstSuffix"` // This is the optional suffix after the "+" in the MIME type, // e.g. "xml" in "application/rss+xml". mimeSuffix string - Delimiter string `json:"delimiter"` // e.g. "." - - Suffixes []string `json:"suffixes"` - - // Set when doing lookup by suffix. - fileSuffix string + // E.g. "jpg,jpeg" + // Stored as a string to make Type comparable. + suffixesCSV string } -// FromStringAndExt is same as FromString, but adds the file extension to the type. +// SuffixInfo holds information about a Type's suffix. +type SuffixInfo struct { + Suffix string `json:"suffix"` + FullSuffix string `json:"fullSuffix"` +} + +// FromStringAndExt creates a Type from a MIME string and a given extension. func FromStringAndExt(t, ext string) (Type, error) { tp, err := fromString(t) if err != nil { return tp, err } - tp.Suffixes = []string{strings.TrimPrefix(ext, ".")} + tp.suffixesCSV = strings.TrimPrefix(ext, ".") + tp.Delimiter = defaultDelimiter + tp.init() return tp, nil } @@ -102,61 +113,83 @@ func (m Type) String() string { return m.Type() } -// FullSuffix returns the file suffix with any delimiter prepended. -func (m Type) FullSuffix() string { - return m.Delimiter + m.Suffix() +// Suffixes returns all valid file suffixes for this type. +func (m Type) Suffixes() []string { + if m.suffixesCSV == "" { + return nil + } + + return strings.Split(m.suffixesCSV, ",") } -// Suffix returns the file suffix without any delimiter prepended. -func (m Type) Suffix() string { - if m.fileSuffix != "" { - return m.fileSuffix +func (m *Type) init() { + m.FirstSuffix.FullSuffix = "" + m.FirstSuffix.Suffix = "" + if suffixes := m.Suffixes(); suffixes != nil { + m.FirstSuffix.Suffix = suffixes[0] + m.FirstSuffix.FullSuffix = m.Delimiter + m.FirstSuffix.Suffix } - if len(m.Suffixes) > 0 { - return m.Suffixes[0] - } - // There are MIME types without file suffixes. - return "" +} + +// WithDelimiterAndSuffixes is used in tests. +func WithDelimiterAndSuffixes(t Type, delimiter, suffixesCSV string) Type { + t.Delimiter = delimiter + 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.init() + return t +} + +func newMediaTypeWithMimeSuffix(main, sub, mimeSuffix string, suffixes []string) Type { + mt := newMediaType(main, sub, suffixes) + mt.mimeSuffix = mimeSuffix + mt.init() + 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 = Type{MainType: "text", SubType: "calendar", Suffixes: []string{"ics"}, Delimiter: defaultDelimiter} - CSSType = Type{MainType: "text", SubType: "css", Suffixes: []string{"css"}, Delimiter: defaultDelimiter} - SCSSType = Type{MainType: "text", SubType: "x-scss", Suffixes: []string{"scss"}, Delimiter: defaultDelimiter} - SASSType = Type{MainType: "text", SubType: "x-sass", Suffixes: []string{"sass"}, Delimiter: defaultDelimiter} - CSVType = Type{MainType: "text", SubType: "csv", Suffixes: []string{"csv"}, Delimiter: defaultDelimiter} - HTMLType = Type{MainType: "text", SubType: "html", Suffixes: []string{"html"}, Delimiter: defaultDelimiter} - JavascriptType = Type{MainType: "application", SubType: "javascript", Suffixes: []string{"js"}, Delimiter: defaultDelimiter} - TypeScriptType = Type{MainType: "application", SubType: "typescript", Suffixes: []string{"ts"}, Delimiter: defaultDelimiter} - TSXType = Type{MainType: "text", SubType: "tsx", Suffixes: []string{"tsx"}, Delimiter: defaultDelimiter} - JSXType = Type{MainType: "text", SubType: "jsx", Suffixes: []string{"jsx"}, Delimiter: defaultDelimiter} + 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("application", "javascript", []string{"js"}) + TypeScriptType = newMediaType("application", "typescript", []string{"ts"}) + TSXType = newMediaType("text", "tsx", []string{"tsx"}) + JSXType = newMediaType("text", "jsx", []string{"jsx"}) - JSONType = Type{MainType: "application", SubType: "json", Suffixes: []string{"json"}, Delimiter: defaultDelimiter} - RSSType = Type{MainType: "application", SubType: "rss", mimeSuffix: "xml", Suffixes: []string{"xml"}, Delimiter: defaultDelimiter} - XMLType = Type{MainType: "application", SubType: "xml", Suffixes: []string{"xml"}, Delimiter: defaultDelimiter} - SVGType = Type{MainType: "image", SubType: "svg", mimeSuffix: "xml", Suffixes: []string{"svg"}, Delimiter: defaultDelimiter} - TextType = Type{MainType: "text", SubType: "plain", Suffixes: []string{"txt"}, Delimiter: defaultDelimiter} - TOMLType = Type{MainType: "application", SubType: "toml", Suffixes: []string{"toml"}, Delimiter: defaultDelimiter} - YAMLType = Type{MainType: "application", SubType: "yaml", Suffixes: []string{"yaml", "yml"}, Delimiter: defaultDelimiter} + JSONType = newMediaType("application", "json", []string{"json"}) + RSSType = newMediaTypeWithMimeSuffix("application", "rss", "xml", []string{"xml"}) + 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 = Type{MainType: "image", SubType: "png", Suffixes: []string{"png"}, Delimiter: defaultDelimiter} - JPEGType = Type{MainType: "image", SubType: "jpeg", Suffixes: []string{"jpg", "jpeg"}, Delimiter: defaultDelimiter} - GIFType = Type{MainType: "image", SubType: "gif", Suffixes: []string{"gif"}, Delimiter: defaultDelimiter} - TIFFType = Type{MainType: "image", SubType: "tiff", Suffixes: []string{"tif", "tiff"}, Delimiter: defaultDelimiter} - BMPType = Type{MainType: "image", SubType: "bmp", Suffixes: []string{"bmp"}, Delimiter: defaultDelimiter} + PNGType = newMediaType("image", "png", []string{"png"}) + JPEGType = newMediaType("image", "jpeg", []string{"jpg", "jpeg"}) + GIFType = newMediaType("image", "gif", []string{"gif"}) + TIFFType = newMediaType("image", "tiff", []string{"tif", "tiff"}) + BMPType = newMediaType("image", "bmp", []string{"bmp"}) // Common video types - AVIType = Type{MainType: "video", SubType: "x-msvideo", Suffixes: []string{"avi"}, Delimiter: defaultDelimiter} - MPEGType = Type{MainType: "video", SubType: "mpeg", Suffixes: []string{"mpg", "mpeg"}, Delimiter: defaultDelimiter} - MP4Type = Type{MainType: "video", SubType: "mp4", Suffixes: []string{"mp4"}, Delimiter: defaultDelimiter} - OGGType = Type{MainType: "video", SubType: "ogg", Suffixes: []string{"ogv"}, Delimiter: defaultDelimiter} - WEBMType = Type{MainType: "video", SubType: "webm", Suffixes: []string{"webm"}, Delimiter: defaultDelimiter} - GPPType = Type{MainType: "video", SubType: "3gpp", Suffixes: []string{"3gpp", "3gp"}, Delimiter: defaultDelimiter} + 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 = Type{MainType: "application", SubType: "octet-stream"} + OctetType = newMediaType("application", "octet-stream", nil) ) // DefaultTypes is the default media types supported by Hugo. @@ -221,54 +254,56 @@ func (t Types) GetByType(tp string) (Type, bool) { // BySuffix will return all media types matching a suffix. func (t Types) BySuffix(suffix string) []Type { + suffix = strings.ToLower(suffix) var types []Type for _, tt := range t { - if match := tt.matchSuffix(suffix); match != "" { + if tt.hasSuffix(suffix) { types = append(types, tt) } } return types } -// GetFirstBySuffix will return the first media type matching the given suffix. -func (t Types) GetFirstBySuffix(suffix string) (Type, bool) { +// GetFirstBySuffix will return the first type matching the given suffix. +func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) { + suffix = strings.ToLower(suffix) for _, tt := range t { - if match := tt.matchSuffix(suffix); match != "" { - tt.fileSuffix = match - return tt, true + if tt.hasSuffix(suffix) { + return tt, SuffixInfo{ + FullSuffix: tt.Delimiter + suffix, + Suffix: suffix, + }, true } } - return Type{}, false + return Type{}, SuffixInfo{}, false } // GetBySuffix gets a media type given as suffix, e.g. "html". // It will return false if no format could be found, or if the suffix given // is ambiguous. // The lookup is case insensitive. -func (t Types) GetBySuffix(suffix string) (tp Type, found bool) { +func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) { + suffix = strings.ToLower(suffix) for _, tt := range t { - if match := tt.matchSuffix(suffix); match != "" { + if tt.hasSuffix(suffix) { if found { // ambiguous found = false return } tp = tt - tp.fileSuffix = match + si = SuffixInfo{ + FullSuffix: tt.Delimiter + suffix, + Suffix: suffix, + } found = true } } return } -func (m Type) matchSuffix(suffix string) string { - for _, s := range m.Suffixes { - if strings.EqualFold(suffix, s) { - return s - } - } - - return "" +func (m Type) hasSuffix(suffix string) bool { + return strings.Contains(m.suffixesCSV, suffix) } // GetByMainSubType gets a media type given a main and a sub type e.g. "text" and "plain". @@ -328,9 +363,6 @@ func DecodeTypes(mms ...map[string]interface{}) (Types, error) { // Maps type string to Type. Type string is the full application/svg+xml. mmm := make(map[string]Type) for _, dt := range DefaultTypes { - suffixes := make([]string, len(dt.Suffixes)) - copy(suffixes, dt.Suffixes) - dt.Suffixes = suffixes mmm[dt.Type()] = dt } @@ -360,11 +392,17 @@ func DecodeTypes(mms ...map[string]interface{}) (Types, error) { 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 && len(mediaType.Suffixes) != 0 { + if !delimiterSet && mediaType.suffixesCSV != "" { mediaType.Delimiter = defaultDelimiter } + mediaType.init() + mmm[k] = mediaType } @@ -387,12 +425,14 @@ func (m Type) IsZero() bool { func (m Type) MarshalJSON() ([]byte, error) { type Alias Type return json.Marshal(&struct { - Type string `json:"type"` - String string `json:"string"` Alias + Type string `json:"type"` + String string `json:"string"` + Suffixes []string `json:"suffixes"` }{ - Type: m.Type(), - String: m.String(), - Alias: (Alias)(m), + Alias: (Alias)(m), + Type: m.Type(), + String: m.String(), + Suffixes: strings.Split(m.suffixesCSV, ","), }) } diff --git a/media/mediaType_test.go b/media/mediaType_test.go index a846ac6ad..e44ab27ec 100644 --- a/media/mediaType_test.go +++ b/media/mediaType_test.go @@ -14,16 +14,12 @@ package media import ( + "encoding/json" "testing" qt "github.com/frankban/quicktest" - "github.com/google/go-cmp/cmp" ) -var eq = qt.CmpEquals(cmp.Comparer(func(m1, m2 Type) bool { - return m1.Type() == m2.Type() -})) - func TestDefaultTypes(t *testing.T) { c := qt.New(t) for _, test := range []struct { @@ -53,8 +49,6 @@ func TestDefaultTypes(t *testing.T) { } { c.Assert(test.tp.MainType, qt.Equals, test.expectedMainType) c.Assert(test.tp.SubType, qt.Equals, test.expectedSubType) - c.Assert(test.tp.Suffix(), qt.Equals, test.expectedSuffix) - c.Assert(test.tp.Delimiter, qt.Equals, defaultDelimiter) c.Assert(test.tp.Type(), qt.Equals, test.expectedType) c.Assert(test.tp.String(), qt.Equals, test.expectedString) @@ -71,25 +65,25 @@ func TestGetByType(t *testing.T) { mt, found := types.GetByType("text/HTML") c.Assert(found, qt.Equals, true) - c.Assert(HTMLType, eq, mt) + c.Assert(HTMLType, qt.Equals, mt) _, 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, eq, mt) + c.Assert(RSSType, qt.Equals, mt) mt, found = types.GetByType("application/rss") c.Assert(found, qt.Equals, true) - c.Assert(RSSType, eq, mt) + c.Assert(RSSType, qt.Equals, mt) } func TestGetByMainSubType(t *testing.T) { c := qt.New(t) f, found := DefaultTypes.GetByMainSubType("text", "plain") c.Assert(found, qt.Equals, true) - c.Assert(TextType, eq, f) + c.Assert(f, qt.Equals, TextType) _, found = DefaultTypes.GetByMainSubType("foo", "plain") c.Assert(found, qt.Equals, false) } @@ -104,48 +98,63 @@ func TestBySuffix(t *testing.T) { func TestGetFirstBySuffix(t *testing.T) { c := qt.New(t) - f, found := DefaultTypes.GetFirstBySuffix("xml") + _, f, found := DefaultTypes.GetFirstBySuffix("xml") c.Assert(found, qt.Equals, true) - c.Assert(f, eq, Type{MainType: "application", SubType: "rss", mimeSuffix: "xml", Delimiter: ".", Suffixes: []string{"xml"}, fileSuffix: "xml"}) + c.Assert(f, qt.Equals, SuffixInfo{ + Suffix: "xml", + FullSuffix: ".xml"}) } func TestFromTypeString(t *testing.T) { c := qt.New(t) f, err := fromString("text/html") c.Assert(err, qt.IsNil) - c.Assert(f.Type(), eq, HTMLType.Type()) + c.Assert(f.Type(), qt.Equals, HTMLType.Type()) f, err = fromString("application/custom") c.Assert(err, qt.IsNil) - c.Assert(f, eq, Type{MainType: "application", SubType: "custom", mimeSuffix: "", fileSuffix: ""}) + c.Assert(f, qt.Equals, Type{MainType: "application", SubType: "custom", mimeSuffix: ""}) f, err = fromString("application/custom+sfx") c.Assert(err, qt.IsNil) - c.Assert(f, eq, Type{MainType: "application", SubType: "custom", mimeSuffix: "sfx"}) + c.Assert(f, qt.Equals, Type{MainType: "application", SubType: "custom", mimeSuffix: "sfx"}) _, err = fromString("noslash") c.Assert(err, qt.Not(qt.IsNil)) f, err = fromString("text/xml; charset=utf-8") c.Assert(err, qt.IsNil) - c.Assert(f, eq, Type{MainType: "text", SubType: "xml", mimeSuffix: ""}) - c.Assert(f.Suffix(), qt.Equals, "") + + c.Assert(f, qt.Equals, Type{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) + f, err = FromStringAndExt("text/html", ".html") + c.Assert(err, qt.IsNil) + c.Assert(f, qt.Equals, HTMLType) } // Add a test for the SVG case // https://github.com/gohugoio/hugo/issues/4920 func TestFromExtensionMultipleSuffixes(t *testing.T) { c := qt.New(t) - tp, found := DefaultTypes.GetBySuffix("svg") + tp, si, found := DefaultTypes.GetBySuffix("svg") c.Assert(found, qt.Equals, true) c.Assert(tp.String(), qt.Equals, "image/svg+xml") - c.Assert(tp.fileSuffix, qt.Equals, "svg") - c.Assert(tp.FullSuffix(), qt.Equals, ".svg") - tp, found = DefaultTypes.GetByType("image/svg+xml") + c.Assert(si.Suffix, qt.Equals, "svg") + c.Assert(si.FullSuffix, qt.Equals, ".svg") + c.Assert(tp.FirstSuffix.Suffix, qt.Equals, si.Suffix) + c.Assert(tp.FirstSuffix.FullSuffix, qt.Equals, si.FullSuffix) + ftp, found := DefaultTypes.GetByType("image/svg+xml") c.Assert(found, qt.Equals, true) - c.Assert(tp.String(), qt.Equals, "image/svg+xml") + c.Assert(ftp.String(), qt.Equals, "image/svg+xml") c.Assert(found, qt.Equals, true) - c.Assert(tp.FullSuffix(), qt.Equals, ".svg") + } func TestDecodeTypes(t *testing.T) { @@ -169,10 +178,10 @@ func TestDecodeTypes(t *testing.T) { false, func(t *testing.T, name string, tt Types) { c.Assert(len(tt), qt.Equals, len(DefaultTypes)) - json, found := tt.GetBySuffix("jasn") + json, si, found := tt.GetBySuffix("jasn") c.Assert(found, qt.Equals, true) c.Assert(json.String(), qt.Equals, "application/json") - c.Assert(json.FullSuffix(), qt.Equals, ".jasn") + c.Assert(si.FullSuffix, qt.Equals, ".jasn") }, }, { @@ -180,7 +189,7 @@ func TestDecodeTypes(t *testing.T) { []map[string]interface{}{ { "application/hugo+hg": map[string]interface{}{ - "suffixes": []string{"hg1", "hg2"}, + "suffixes": []string{"hg1", "hG2"}, "Delimiter": "_", }, }, @@ -188,15 +197,18 @@ func TestDecodeTypes(t *testing.T) { false, func(t *testing.T, name string, tt Types) { c.Assert(len(tt), qt.Equals, len(DefaultTypes)+1) - hg, found := tt.GetBySuffix("hg2") + hg, si, found := tt.GetBySuffix("hg2") c.Assert(found, qt.Equals, true) c.Assert(hg.mimeSuffix, qt.Equals, "hg") - c.Assert(hg.Suffix(), qt.Equals, "hg2") - c.Assert(hg.FullSuffix(), qt.Equals, "_hg2") + 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") - hg, found = tt.GetByType("application/hugo+hg") + _, found = tt.GetByType("application/hugo+hg") c.Assert(found, qt.Equals, true) + }, }, { @@ -209,14 +221,14 @@ func TestDecodeTypes(t *testing.T) { }, }, false, - func(t *testing.T, name string, tt Types) { - c.Assert(len(tt), qt.Equals, len(DefaultTypes)+1) + 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 := tt.GetBySuffix("json") + _, _, found := tp.GetBySuffix("json") c.Assert(found, qt.Equals, true) - hugo, found := tt.GetBySuffix("hgo2") + hugo, _, found := tp.GetBySuffix("hgo2") c.Assert(found, qt.Equals, true) c.Assert(hugo.String(), qt.Equals, "text/hugo+hgo") }, @@ -234,25 +246,33 @@ func TestDecodeTypes(t *testing.T) { } } +func TestToJSON(t *testing.T) { + c := qt.New(t) + b, err := json.Marshal(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"]}`) +} + func BenchmarkTypeOps(b *testing.B) { mt := MPEGType mts := DefaultTypes for i := 0; i < b.N; i++ { - _ = mt.FullSuffix() + ff := mt.FirstSuffix + _ = ff.FullSuffix _ = mt.IsZero() c, err := mt.MarshalJSON() if c == nil || err != nil { b.Fatal("failed") } _ = mt.String() - _ = mt.Suffix() + _ = ff.Suffix _ = mt.Suffixes _ = mt.Type() _ = mts.BySuffix("xml") _, _ = mts.GetByMainSubType("application", "xml") - _, _ = mts.GetBySuffix("xml") + _, _, _ = mts.GetBySuffix("xml") _, _ = mts.GetByType("application") - _, _ = mts.GetFirstBySuffix("xml") + _, _, _ = mts.GetFirstBySuffix("xml") } } diff --git a/output/docshelper.go b/output/docshelper.go index 42b871ab5..95e521ea3 100644 --- a/output/docshelper.go +++ b/output/docshelper.go @@ -76,7 +76,7 @@ func createLayoutExamples() interface{} { Example: example.name, Kind: example.d.Kind, OutputFormat: example.f.Name, - Suffix: example.f.MediaType.Suffix(), + Suffix: example.f.MediaType.FirstSuffix.Suffix, Layouts: makeLayoutsPresentable(layouts), }) } diff --git a/output/layout.go b/output/layout.go index 55e7fa305..91c7cc652 100644 --- a/output/layout.go +++ b/output/layout.go @@ -229,7 +229,7 @@ func (l *layoutBuilder) resolveVariations() []string { continue } - s := constructLayoutPath(typeVar, layoutVar, variation, l.f.MediaType.Suffix()) + s := constructLayoutPath(typeVar, layoutVar, variation, l.f.MediaType.FirstSuffix.Suffix) if s != "" { layouts = append(layouts, s) } diff --git a/output/layout_test.go b/output/layout_test.go index f98eea960..8b7a2b541 100644 --- a/output/layout_test.go +++ b/output/layout_test.go @@ -28,12 +28,8 @@ import ( func TestLayout(t *testing.T) { c := qt.New(t) - noExtNoDelimMediaType := media.TextType - noExtNoDelimMediaType.Suffixes = nil - noExtNoDelimMediaType.Delimiter = "" - - noExtMediaType := media.TextType - noExtMediaType.Suffixes = nil + noExtNoDelimMediaType := media.WithDelimiterAndSuffixes(media.TextType, "", "") + noExtMediaType := media.WithDelimiterAndSuffixes(media.TextType, ".", "") var ( ampType = Format{ diff --git a/output/outputFormat.go b/output/outputFormat.go index ee5881e2a..1bd86d2ca 100644 --- a/output/outputFormat.go +++ b/output/outputFormat.go @@ -20,6 +20,8 @@ import ( "sort" "strings" + "github.com/pkg/errors" + "github.com/mitchellh/mapstructure" "github.com/gohugoio/hugo/media" @@ -207,14 +209,16 @@ func (formats Formats) Less(i, j int) bool { // The lookup is case insensitive. func (formats Formats) GetBySuffix(suffix string) (f Format, found bool) { for _, ff := range formats { - if strings.EqualFold(suffix, ff.MediaType.Suffix()) { - if found { - // ambiguous - found = false - return + for _, suffix2 := range ff.MediaType.Suffixes() { + if strings.EqualFold(suffix, suffix2) { + if found { + // ambiguous + found = false + return + } + f = ff + found = true } - f = ff - found = true } } return @@ -310,6 +314,7 @@ func DecodeFormats(mediaTypes media.Types, maps ...map[string]interface{}) (Form } f = append(f, newOutFormat) + } } } @@ -319,7 +324,7 @@ func DecodeFormats(mediaTypes media.Types, maps ...map[string]interface{}) (Form return f, nil } -func decode(mediaTypes media.Types, input, output interface{}) error { +func decode(mediaTypes media.Types, input interface{}, output *Format) error { config := &mapstructure.DecoderConfig{ Metadata: nil, Result: output, @@ -337,12 +342,19 @@ func decode(mediaTypes media.Types, input, output interface{}) error { // If mediaType is a string, look it up and replace it // in the map. vv := dataVal.MapIndex(key) - if mediaTypeStr, ok := vv.Interface().(string); ok { - mediaType, found := mediaTypes.GetByType(mediaTypeStr) + vvi := vv.Interface() + + switch vviv := vvi.(type) { + case media.Type: + // OK + case string: + mediaType, found := mediaTypes.GetByType(vviv) if !found { - return c, fmt.Errorf("media type %q not found", mediaTypeStr) + return c, fmt.Errorf("media type %q not found", vviv) } dataVal.SetMapIndex(key, reflect.ValueOf(mediaType)) + default: + return nil, errors.Errorf("invalid output format configuration; wrong type for media type, expected string (e.g. text/html), got %T", vvi) } } } @@ -357,12 +369,13 @@ func decode(mediaTypes media.Types, input, output interface{}) error { } return decoder.Decode(input) + } // BaseFilename returns the base filename of f including an extension (ie. // "index.xml"). func (f Format) BaseFilename() string { - return f.BaseName + f.MediaType.FullSuffix() + return f.BaseName + f.MediaType.FirstSuffix.FullSuffix } // MarshalJSON returns the JSON encoding of f. diff --git a/output/outputFormat_test.go b/output/outputFormat_test.go index d91f1e410..806998866 100644 --- a/output/outputFormat_test.go +++ b/output/outputFormat_test.go @@ -19,36 +19,26 @@ import ( qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/media" - "github.com/google/go-cmp/cmp" -) - -var eq = qt.CmpEquals( - cmp.Comparer(func(m1, m2 media.Type) bool { - return m1.Type() == m2.Type() - }), - cmp.Comparer(func(o1, o2 Format) bool { - return o1.Name == o2.Name - }), ) func TestDefaultTypes(t *testing.T) { c := qt.New(t) c.Assert(CalendarFormat.Name, qt.Equals, "Calendar") - c.Assert(CalendarFormat.MediaType, eq, media.CalendarType) + c.Assert(CalendarFormat.MediaType, qt.Equals, media.CalendarType) c.Assert(CalendarFormat.Protocol, qt.Equals, "webcal://") c.Assert(CalendarFormat.Path, qt.HasLen, 0) c.Assert(CalendarFormat.IsPlainText, qt.Equals, true) c.Assert(CalendarFormat.IsHTML, qt.Equals, false) c.Assert(CSSFormat.Name, qt.Equals, "CSS") - c.Assert(CSSFormat.MediaType, eq, media.CSSType) + c.Assert(CSSFormat.MediaType, qt.Equals, media.CSSType) c.Assert(CSSFormat.Path, qt.HasLen, 0) c.Assert(CSSFormat.Protocol, qt.HasLen, 0) // Will inherit the BaseURL protocol. c.Assert(CSSFormat.IsPlainText, qt.Equals, true) c.Assert(CSSFormat.IsHTML, qt.Equals, false) c.Assert(CSVFormat.Name, qt.Equals, "CSV") - c.Assert(CSVFormat.MediaType, eq, media.CSVType) + c.Assert(CSVFormat.MediaType, qt.Equals, media.CSVType) c.Assert(CSVFormat.Path, qt.HasLen, 0) c.Assert(CSVFormat.Protocol, qt.HasLen, 0) c.Assert(CSVFormat.IsPlainText, qt.Equals, true) @@ -56,7 +46,7 @@ func TestDefaultTypes(t *testing.T) { c.Assert(CSVFormat.Permalinkable, qt.Equals, false) c.Assert(HTMLFormat.Name, qt.Equals, "HTML") - c.Assert(HTMLFormat.MediaType, eq, media.HTMLType) + c.Assert(HTMLFormat.MediaType, qt.Equals, media.HTMLType) c.Assert(HTMLFormat.Path, qt.HasLen, 0) c.Assert(HTMLFormat.Protocol, qt.HasLen, 0) c.Assert(HTMLFormat.IsPlainText, qt.Equals, false) @@ -64,7 +54,7 @@ func TestDefaultTypes(t *testing.T) { c.Assert(AMPFormat.Permalinkable, qt.Equals, true) c.Assert(AMPFormat.Name, qt.Equals, "AMP") - c.Assert(AMPFormat.MediaType, eq, media.HTMLType) + c.Assert(AMPFormat.MediaType, qt.Equals, media.HTMLType) c.Assert(AMPFormat.Path, qt.Equals, "amp") c.Assert(AMPFormat.Protocol, qt.HasLen, 0) c.Assert(AMPFormat.IsPlainText, qt.Equals, false) @@ -72,7 +62,7 @@ func TestDefaultTypes(t *testing.T) { c.Assert(AMPFormat.Permalinkable, qt.Equals, true) c.Assert(RSSFormat.Name, qt.Equals, "RSS") - c.Assert(RSSFormat.MediaType, eq, media.RSSType) + c.Assert(RSSFormat.MediaType, qt.Equals, media.RSSType) c.Assert(RSSFormat.Path, qt.HasLen, 0) c.Assert(RSSFormat.IsPlainText, qt.Equals, false) c.Assert(RSSFormat.NoUgly, qt.Equals, true) @@ -83,7 +73,7 @@ func TestGetFormatByName(t *testing.T) { c := qt.New(t) formats := Formats{AMPFormat, CalendarFormat} tp, _ := formats.GetByName("AMp") - c.Assert(tp, eq, AMPFormat) + c.Assert(tp, qt.Equals, AMPFormat) _, found := formats.GetByName("HTML") c.Assert(found, qt.Equals, false) _, found = formats.GetByName("FOO") @@ -95,9 +85,9 @@ func TestGetFormatByExt(t *testing.T) { formats1 := Formats{AMPFormat, CalendarFormat} formats2 := Formats{AMPFormat, HTMLFormat, CalendarFormat} tp, _ := formats1.GetBySuffix("html") - c.Assert(tp, eq, AMPFormat) + c.Assert(tp, qt.Equals, AMPFormat) tp, _ = formats1.GetBySuffix("ics") - c.Assert(tp, eq, CalendarFormat) + c.Assert(tp, qt.Equals, CalendarFormat) _, found := formats1.GetBySuffix("not") c.Assert(found, qt.Equals, false) @@ -129,18 +119,18 @@ func TestGetFormatByFilename(t *testing.T) { formats := Formats{AMPFormat, HTMLFormat, noExtDelimFormat, noExt, CalendarFormat} f, found := formats.FromFilename("my.amp.html") c.Assert(found, qt.Equals, true) - c.Assert(f, eq, AMPFormat) + c.Assert(f, qt.Equals, AMPFormat) _, found = formats.FromFilename("my.ics") c.Assert(found, qt.Equals, true) f, found = formats.FromFilename("my.html") c.Assert(found, qt.Equals, true) - c.Assert(f, eq, HTMLFormat) + c.Assert(f, qt.Equals, HTMLFormat) f, found = formats.FromFilename("my.nem") c.Assert(found, qt.Equals, true) - c.Assert(f, eq, noExtDelimFormat) + c.Assert(f, qt.Equals, noExtDelimFormat) f, found = formats.FromFilename("my.nex") c.Assert(found, qt.Equals, true) - c.Assert(f, eq, noExt) + c.Assert(f, qt.Equals, noExt) _, found = formats.FromFilename("my.css") c.Assert(found, qt.Equals, false) } @@ -172,7 +162,7 @@ func TestDecodeFormats(t *testing.T) { c.Assert(len(f), qt.Equals, len(DefaultFormats), msg) json, _ := f.GetByName("JSON") c.Assert(json.BaseName, qt.Equals, "myindex") - c.Assert(json.MediaType, eq, media.JSONType) + c.Assert(json.MediaType, qt.Equals, media.JSONType) c.Assert(json.IsPlainText, qt.Equals, false) }, }, @@ -192,7 +182,7 @@ func TestDecodeFormats(t *testing.T) { xml, found := f.GetByName("MYXMLFORMAT") c.Assert(found, qt.Equals, true) c.Assert(xml.BaseName, qt.Equals, "myxml") - c.Assert(xml.MediaType, eq, media.XMLType) + c.Assert(xml.MediaType, qt.Equals, media.XMLType) // Verify that we haven't changed the DefaultFormats slice. json, _ := f.GetByName("JSON") @@ -234,7 +224,7 @@ func TestDecodeFormats(t *testing.T) { xml, found := f.GetByName("MYOTHERXMLFORMAT") c.Assert(found, qt.Equals, true) c.Assert(xml.BaseName, qt.Equals, "myredefined") - c.Assert(xml.MediaType, eq, media.XMLType) + c.Assert(xml.MediaType, qt.Equals, media.XMLType) }, }, } diff --git a/parser/metadecoders/format.go b/parser/metadecoders/format.go index d2b35223d..bba89dbea 100644 --- a/parser/metadecoders/format.go +++ b/parser/metadecoders/format.go @@ -59,7 +59,7 @@ func FormatFromString(formatStr string) Format { // FormatFromMediaType gets the Format given a MIME type, empty string // if unknown. func FormatFromMediaType(m media.Type) Format { - for _, suffix := range m.Suffixes { + for _, suffix := range m.Suffixes() { if f := FormatFromString(suffix); f != "" { return f } diff --git a/resources/images/image.go b/resources/images/image.go index 3d28263c0..b71321244 100644 --- a/resources/images/image.go +++ b/resources/images/image.go @@ -265,7 +265,7 @@ func (f Format) SupportsTransparency() bool { // DefaultExtension returns the default file extension of this format, starting with a dot. // For example: .jpg for JPEG func (f Format) DefaultExtension() string { - return f.MediaType().FullSuffix() + return f.MediaType().FirstSuffix.FullSuffix } // MediaType returns the media type of this image, e.g. image/jpeg for JPEG diff --git a/resources/page/page_paths.go b/resources/page/page_paths.go index fc576f05a..3d34866d1 100644 --- a/resources/page/page_paths.go +++ b/resources/page/page_paths.go @@ -128,6 +128,7 @@ func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) { } pagePath := slash + fullSuffix := d.Type.MediaType.FirstSuffix.FullSuffix var ( pagePathDir string @@ -172,7 +173,7 @@ func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) { hasSlash := strings.HasSuffix(d.URL, slash) if hasSlash || !hasDot { - pagePath = pjoin(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix()) + pagePath = pjoin(pagePath, d.Type.BaseName+fullSuffix) } else if hasDot { pagePathDir = path.Dir(pagePathDir) } @@ -229,9 +230,9 @@ func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) { linkDir = pagePathDir if isUgly { - pagePath = addSuffix(pagePath, d.Type.MediaType.FullSuffix()) + pagePath = addSuffix(pagePath, fullSuffix) } else { - pagePath = pjoin(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix()) + pagePath = pjoin(pagePath, d.Type.BaseName+fullSuffix) } if !isHtmlIndex(pagePath) { @@ -267,9 +268,9 @@ func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) { linkDir = pagePathDir if base != "" { - pagePath = path.Join(pagePath, addSuffix(base, d.Type.MediaType.FullSuffix())) + pagePath = path.Join(pagePath, addSuffix(base, fullSuffix)) } else { - pagePath = addSuffix(pagePath, d.Type.MediaType.FullSuffix()) + pagePath = addSuffix(pagePath, fullSuffix) } if !isHtmlIndex(pagePath) { diff --git a/resources/page/page_paths_test.go b/resources/page/page_paths_test.go index ab7164f46..28937899f 100644 --- a/resources/page/page_paths_test.go +++ b/resources/page/page_paths_test.go @@ -27,8 +27,7 @@ import ( func TestPageTargetPath(t *testing.T) { pathSpec := newTestPathSpec() - noExtNoDelimMediaType := media.TextType - noExtNoDelimMediaType.Suffixes = []string{} + noExtNoDelimMediaType := media.WithDelimiterAndSuffixes(media.TextType, "", "") noExtNoDelimMediaType.Delimiter = "" // Netlify style _redirects @@ -209,11 +208,11 @@ func TestPageTargetPath(t *testing.T) { // TODO(bep) simplify if test.d.Kind == KindPage && test.d.BaseName == test.d.Type.BaseName { } else if test.d.Kind == KindHome && test.d.Type.Path != "" { - } else if test.d.Type.MediaType.Suffix() != "" && (!strings.HasPrefix(expected.TargetFilename, "/index") || test.d.Addends != "") && test.d.URL == "" && isUgly { + } else if test.d.Type.MediaType.FirstSuffix.Suffix != "" && (!strings.HasPrefix(expected.TargetFilename, "/index") || test.d.Addends != "") && test.d.URL == "" && isUgly { expected.TargetFilename = strings.Replace(expected.TargetFilename, - "/"+test.d.Type.BaseName+"."+test.d.Type.MediaType.Suffix(), - "."+test.d.Type.MediaType.Suffix(), 1) - expected.Link = strings.TrimSuffix(expected.Link, "/") + "." + test.d.Type.MediaType.Suffix() + "/"+test.d.Type.BaseName+"."+test.d.Type.MediaType.FirstSuffix.Suffix, + "."+test.d.Type.MediaType.FirstSuffix.Suffix, 1) + expected.Link = strings.TrimSuffix(expected.Link, "/") + "." + test.d.Type.MediaType.FirstSuffix.Suffix } diff --git a/resources/postpub/fields_test.go b/resources/postpub/fields_test.go index 10a68ac5a..19c3720f7 100644 --- a/resources/postpub/fields_test.go +++ b/resources/postpub/fields_test.go @@ -31,15 +31,14 @@ func TestCreatePlaceholders(t *testing.T) { }) c.Assert(m, qt.DeepEquals, map[string]interface{}{ - "FullSuffix": "pre_foo.FullSuffix_post", "IsZero": "pre_foo.IsZero_post", + "MarshalJSON": "pre_foo.MarshalJSON_post", + "Suffixes": "pre_foo.Suffixes_post", + "Delimiter": "pre_foo.Delimiter_post", + "FirstSuffix": "pre_foo.FirstSuffix_post", + "String": "pre_foo.String_post", "Type": "pre_foo.Type_post", "MainType": "pre_foo.MainType_post", - "Delimiter": "pre_foo.Delimiter_post", - "MarshalJSON": "pre_foo.MarshalJSON_post", - "String": "pre_foo.String_post", - "Suffix": "pre_foo.Suffix_post", "SubType": "pre_foo.SubType_post", - "Suffixes": "pre_foo.Suffixes_post", }) } diff --git a/resources/resource_spec.go b/resources/resource_spec.go index dc13d16d9..156def363 100644 --- a/resources/resource_spec.go +++ b/resources/resource_spec.go @@ -268,10 +268,10 @@ func (r *Spec) newResource(sourceFs afero.Fs, fd ResourceSourceDescriptor) (reso } ext := strings.ToLower(filepath.Ext(fd.RelTargetFilename)) - mimeType, found := r.MediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, ".")) + mimeType, suffixInfo, found := r.MediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, ".")) // TODO(bep) we need to handle these ambiguous types better, but in this context // we most likely want the application/xml type. - if mimeType.Suffix() == "xml" && mimeType.SubType == "rss" { + if suffixInfo.Suffix == "xml" && mimeType.SubType == "rss" { mimeType, found = r.MediaTypes.GetByType("application/xml") } diff --git a/tpl/tplimpl/shortcodes.go b/tpl/tplimpl/shortcodes.go index 789bc07d3..938fc74e2 100644 --- a/tpl/tplimpl/shortcodes.go +++ b/tpl/tplimpl/shortcodes.go @@ -56,7 +56,7 @@ func (s *shortcodeTemplates) fromVariants(variants tpl.TemplateVariants) (shortc return s.fromVariantsSlice([]string{ variants.Language, strings.ToLower(variants.OutputFormat.Name), - variants.OutputFormat.MediaType.Suffix(), + variants.OutputFormat.MediaType.FirstSuffix.Suffix, }) }