diff options
Diffstat (limited to 'media')
-rw-r--r-- | media/mediaType.go | 198 | ||||
-rw-r--r-- | media/mediaType_test.go | 98 |
2 files changed, 178 insertions, 118 deletions
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"` + // E.g. "jpg,jpeg" + // Stored as a string to make Type comparable. + suffixesCSV string +} - // Set when doing lookup by suffix. - fileSuffix string +// SuffixInfo holds information about a Type's suffix. +type SuffixInfo struct { + Suffix string `json:"suffix"` + FullSuffix string `json:"fullSuffix"` } -// FromStringAndExt is same as FromString, but adds the file extension to the type. +// 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 - } - if len(m.Suffixes) > 0 { - return m.Suffixes[0] +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 } - // 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} - - 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} + 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 = 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} - - OctetType = Type{MainType: "application", SubType: "octet-stream"} + 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. @@ -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") } } |