Improve minifier MIME type resolution

This commit also removes the deprecated `Suffix` from MediaType. Now use `Suffixes` and put the MIME type suffix in the type, e.g. `application/svg+xml`.

Fixes #5093
This commit is contained in:
Bjørn Erik Pedersen 2018-08-28 14:18:12 +02:00
parent 6b9934a266
commit ebb56e8bdb
8 changed files with 87 additions and 106 deletions

View file

@ -97,7 +97,7 @@ top = "top"
[mediaTypes] [mediaTypes]
[mediaTypes."text/m1"] [mediaTypes."text/m1"]
suffix = "m1main" suffixes = ["m1main"]
[outputFormats.o1] [outputFormats.o1]
mediaType = "text/m1" mediaType = "text/m1"
@ -135,9 +135,9 @@ p3 = "p3 theme"
[mediaTypes] [mediaTypes]
[mediaTypes."text/m1"] [mediaTypes."text/m1"]
suffix = "m1theme" suffixes = ["m1theme"]
[mediaTypes."text/m2"] [mediaTypes."text/m2"]
suffix = "m2theme" suffixes = ["m2theme"]
[outputFormats.o1] [outputFormats.o1]
mediaType = "text/m1" mediaType = "text/m1"
@ -207,10 +207,14 @@ map[string]interface {}{
b.AssertObject(` b.AssertObject(`
map[string]interface {}{ map[string]interface {}{
"text/m1": map[string]interface {}{ "text/m1": map[string]interface {}{
"suffix": "m1main", "suffixes": []interface {}{
"m1main",
},
}, },
"text/m2": map[string]interface {}{ "text/m2": map[string]interface {}{
"suffix": "m2theme", "suffixes": []interface {}{
"m2theme",
},
}, },
}`, got["mediatypes"]) }`, got["mediatypes"])
@ -221,7 +225,6 @@ map[string]interface {}{
"mediatype": Type{ "mediatype": Type{
MainType: "text", MainType: "text",
SubType: "m1", SubType: "m1",
OldSuffix: "m1main",
Delimiter: ".", Delimiter: ".",
Suffixes: []string{ Suffixes: []string{
"m1main", "m1main",
@ -233,7 +236,6 @@ map[string]interface {}{
"mediatype": Type{ "mediatype": Type{
MainType: "text", MainType: "text",
SubType: "m2", SubType: "m2",
OldSuffix: "m2theme",
Delimiter: ".", Delimiter: ".",
Suffixes: []string{ Suffixes: []string{
"m2theme", "m2theme",

View file

@ -435,7 +435,7 @@ func newTestBundleSources(t *testing.T) (*hugofs.Fs, *viper.Viper) {
cfg.Set("baseURL", "https://example.com") cfg.Set("baseURL", "https://example.com")
cfg.Set("mediaTypes", map[string]interface{}{ cfg.Set("mediaTypes", map[string]interface{}{
"text/bepsays": map[string]interface{}{ "text/bepsays": map[string]interface{}{
"suffix": "bep", "suffixes": []string{"bep"},
}, },
}) })

View file

@ -276,14 +276,12 @@ disableKinds = ["page", "section", "taxonomy", "taxonomyTerm", "sitemap", "robot
[mediaTypes] [mediaTypes]
[mediaTypes."text/nodot"] [mediaTypes."text/nodot"]
suffix = ""
delimiter = "" delimiter = ""
[mediaTypes."text/defaultdelim"] [mediaTypes."text/defaultdelim"]
suffix = "defd" suffixes = ["defd"]
[mediaTypes."text/nosuffix"] [mediaTypes."text/nosuffix"]
suffix = ""
[mediaTypes."text/customdelim"] [mediaTypes."text/customdelim"]
suffix = "del" suffixes = ["del"]
delimiter = "_" delimiter = "_"
[outputs] [outputs]
@ -321,7 +319,7 @@ baseName = "customdelimbase"
th.assertFileContent("public/_redirects", "a dotless") th.assertFileContent("public/_redirects", "a dotless")
th.assertFileContent("public/defaultdelimbase.defd", "default delimim") th.assertFileContent("public/defaultdelimbase.defd", "default delimim")
// This looks weird, but the user has chosen this definition. // This looks weird, but the user has chosen this definition.
th.assertFileContent("public/nosuffixbase.", "no suffix") th.assertFileContent("public/nosuffixbase", "no suffix")
th.assertFileContent("public/customdelimbase_del", "custom delim") th.assertFileContent("public/customdelimbase_del", "custom delim")
s := h.Sites[0] s := h.Sites[0]
@ -332,7 +330,7 @@ baseName = "customdelimbase"
require.Equal(t, "/blog/_redirects", outputs.Get("DOTLESS").RelPermalink()) require.Equal(t, "/blog/_redirects", outputs.Get("DOTLESS").RelPermalink())
require.Equal(t, "/blog/defaultdelimbase.defd", outputs.Get("DEF").RelPermalink()) require.Equal(t, "/blog/defaultdelimbase.defd", outputs.Get("DEF").RelPermalink())
require.Equal(t, "/blog/nosuffixbase.", outputs.Get("NOS").RelPermalink()) require.Equal(t, "/blog/nosuffixbase", outputs.Get("NOS").RelPermalink())
require.Equal(t, "/blog/customdelimbase_del", outputs.Get("CUS").RelPermalink()) require.Equal(t, "/blog/customdelimbase_del", outputs.Get("CUS").RelPermalink())
} }

View file

@ -15,11 +15,13 @@ package media
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/common/maps"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
) )
@ -37,10 +39,9 @@ type Type struct {
MainType string `json:"mainType"` // i.e. text MainType string `json:"mainType"` // i.e. text
SubType string `json:"subType"` // i.e. html SubType string `json:"subType"` // i.e. html
// Deprecated in Hugo 0.44. To be renamed and unexported. // This is the optional suffix after the "+" in the MIME type,
// Was earlier used both to set file suffix and to augment the MIME type. // e.g. "xml" in "applicatiion/rss+xml".
// This had its limitations and issues. mimeSuffix string
OldSuffix string `json:"-" mapstructure:"suffix"`
Delimiter string `json:"delimiter"` // e.g. "." Delimiter string `json:"delimiter"` // e.g. "."
@ -79,7 +80,7 @@ func fromString(t string) (Type, error) {
suffix = subParts[1] suffix = subParts[1]
} }
return Type{MainType: mainType, SubType: subType, OldSuffix: suffix}, nil 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". // Type returns a string representing the main- and sub-type of a media type, e.g. "text/css".
@ -91,8 +92,8 @@ func (m Type) Type() string {
// Examples are // Examples are
// image/svg+xml // image/svg+xml
// text/css // text/css
if m.OldSuffix != "" { if m.mimeSuffix != "" {
return fmt.Sprintf("%s/%s+%s", m.MainType, m.SubType, m.OldSuffix) return fmt.Sprintf("%s/%s+%s", m.MainType, m.SubType, m.mimeSuffix)
} }
return fmt.Sprintf("%s/%s", m.MainType, m.SubType) return fmt.Sprintf("%s/%s", m.MainType, m.SubType)
@ -130,9 +131,9 @@ var (
HTMLType = Type{MainType: "text", SubType: "html", Suffixes: []string{"html"}, Delimiter: defaultDelimiter} HTMLType = Type{MainType: "text", SubType: "html", Suffixes: []string{"html"}, Delimiter: defaultDelimiter}
JavascriptType = Type{MainType: "application", SubType: "javascript", Suffixes: []string{"js"}, Delimiter: defaultDelimiter} JavascriptType = Type{MainType: "application", SubType: "javascript", Suffixes: []string{"js"}, Delimiter: defaultDelimiter}
JSONType = Type{MainType: "application", SubType: "json", Suffixes: []string{"json"}, Delimiter: defaultDelimiter} JSONType = Type{MainType: "application", SubType: "json", Suffixes: []string{"json"}, Delimiter: defaultDelimiter}
RSSType = Type{MainType: "application", SubType: "rss", OldSuffix: "xml", Suffixes: []string{"xml"}, 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} XMLType = Type{MainType: "application", SubType: "xml", Suffixes: []string{"xml"}, Delimiter: defaultDelimiter}
SVGType = Type{MainType: "image", SubType: "svg", OldSuffix: "xml", Suffixes: []string{"svg"}, 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} TextType = Type{MainType: "text", SubType: "plain", Suffixes: []string{"txt"}, Delimiter: defaultDelimiter}
OctetType = Type{MainType: "application", SubType: "octet-stream"} OctetType = Type{MainType: "application", SubType: "octet-stream"}
@ -182,6 +183,17 @@ func (t Types) GetByType(tp string) (Type, bool) {
return Type{}, false return Type{}, false
} }
// BySuffix will return all media types matching a suffix.
func (t Types) BySuffix(suffix string) []Type {
var types []Type
for _, tt := range t {
if match := tt.matchSuffix(suffix); match != "" {
types = append(types, tt)
}
}
return types
}
// GetFirstBySuffix will return the first media type matching the given suffix. // GetFirstBySuffix will return the first media type matching the given suffix.
func (t Types) GetFirstBySuffix(suffix string) (Type, bool) { func (t Types) GetFirstBySuffix(suffix string) (Type, bool) {
for _, tt := range t { for _, tt := range t {
@ -214,9 +226,6 @@ func (t Types) GetBySuffix(suffix string) (tp Type, found bool) {
} }
func (t Type) matchSuffix(suffix string) string { func (t Type) matchSuffix(suffix string) string {
if strings.EqualFold(suffix, t.OldSuffix) {
return t.OldSuffix
}
for _, s := range t.Suffixes { for _, s := range t.Suffixes {
if strings.EqualFold(suffix, s) { if strings.EqualFold(suffix, s) {
return s return s
@ -246,9 +255,8 @@ func (t Types) GetByMainSubType(mainType, subType string) (tp Type, found bool)
return return
} }
func suffixIsDeprecated() { func suffixIsRemoved() error {
helpers.Deprecated("MediaType", "Suffix in config.toml", ` return errors.New(`MediaType.Suffix is removed. Before Hugo 0.44 this was used both to set a custom file suffix and as way
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"). 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. This had its limitations. For one, it was only possible with one file extension per MIME type.
@ -272,16 +280,13 @@ To:
[mediaTypes."my/custom-mediatype"] [mediaTypes."my/custom-mediatype"]
suffixes = ["txt"] suffixes = ["txt"]
Hugo will still respect values set in "suffix" if no value for "suffixes" is provided, but this will be removed
in a future release.
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. 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.
`, false) `)
} }
// DecodeTypes takes a list of media type configurations and merges those, // DecodeTypes takes a list of media type configurations and merges those,
// in the order given, with the Hugo defaults as the last resort. // in the order given, with the Hugo defaults as the last resort.
func DecodeTypes(maps ...map[string]interface{}) (Types, error) { func DecodeTypes(mms ...map[string]interface{}) (Types, error) {
var m Types var m Types
// Maps type string to Type. Type string is the full application/svg+xml. // Maps type string to Type. Type string is the full application/svg+xml.
@ -293,7 +298,7 @@ func DecodeTypes(maps ...map[string]interface{}) (Types, error) {
mmm[dt.Type()] = dt mmm[dt.Type()] = dt
} }
for _, mm := range maps { for _, mm := range mms {
for k, v := range mm { for k, v := range mm {
var mediaType Type var mediaType Type
@ -311,24 +316,17 @@ func DecodeTypes(maps ...map[string]interface{}) (Types, error) {
} }
vm := v.(map[string]interface{}) vm := v.(map[string]interface{})
maps.ToLower(vm)
_, delimiterSet := vm["delimiter"] _, delimiterSet := vm["delimiter"]
_, suffixSet := vm["suffix"] _, suffixSet := vm["suffix"]
if suffixSet { if suffixSet {
suffixIsDeprecated() return Types{}, suffixIsRemoved()
} }
// Before Hugo 0.44 we had a non-standard use of the Suffix
// attribute, and this is now deprecated (use Suffixes for file suffixes).
// But we need to keep old configurations working for a while.
if len(mediaType.Suffixes) == 0 && mediaType.OldSuffix != "" {
mediaType.Suffixes = []string{mediaType.OldSuffix}
}
// The user may set the delimiter as an empty string. // The user may set the delimiter as an empty string.
if !delimiterSet && len(mediaType.Suffixes) != 0 { if !delimiterSet && len(mediaType.Suffixes) != 0 {
mediaType.Delimiter = defaultDelimiter mediaType.Delimiter = defaultDelimiter
} else if suffixSet && !delimiterSet {
mediaType.Delimiter = defaultDelimiter
} }
mmm[k] = mediaType mmm[k] = mediaType

View file

@ -80,11 +80,19 @@ func TestGetByMainSubType(t *testing.T) {
assert.False(found) assert.False(found)
} }
func TestBySuffix(t *testing.T) {
assert := require.New(t)
formats := DefaultTypes.BySuffix("xml")
assert.Equal(2, len(formats))
assert.Equal("rss", formats[0].SubType)
assert.Equal("xml", formats[1].SubType)
}
func TestGetFirstBySuffix(t *testing.T) { func TestGetFirstBySuffix(t *testing.T) {
assert := require.New(t) assert := require.New(t)
f, found := DefaultTypes.GetFirstBySuffix("xml") f, found := DefaultTypes.GetFirstBySuffix("xml")
assert.True(found) assert.True(found)
assert.Equal(Type{MainType: "application", SubType: "rss", OldSuffix: "xml", Delimiter: ".", Suffixes: []string{"xml"}, fileSuffix: "xml"}, f) assert.Equal(Type{MainType: "application", SubType: "rss", mimeSuffix: "xml", Delimiter: ".", Suffixes: []string{"xml"}, fileSuffix: "xml"}, f)
} }
func TestFromTypeString(t *testing.T) { func TestFromTypeString(t *testing.T) {
@ -94,18 +102,18 @@ func TestFromTypeString(t *testing.T) {
f, err = fromString("application/custom") f, err = fromString("application/custom")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, Type{MainType: "application", SubType: "custom", OldSuffix: "", fileSuffix: ""}, f) require.Equal(t, Type{MainType: "application", SubType: "custom", mimeSuffix: "", fileSuffix: ""}, f)
f, err = fromString("application/custom+sfx") f, err = fromString("application/custom+sfx")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, Type{MainType: "application", SubType: "custom", OldSuffix: "sfx"}, f) require.Equal(t, Type{MainType: "application", SubType: "custom", mimeSuffix: "sfx"}, f)
_, err = fromString("noslash") _, err = fromString("noslash")
require.Error(t, err) require.Error(t, err)
f, err = fromString("text/xml; charset=utf-8") f, err = fromString("text/xml; charset=utf-8")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, Type{MainType: "text", SubType: "xml", OldSuffix: ""}, f) require.Equal(t, Type{MainType: "text", SubType: "xml", mimeSuffix: ""}, f)
require.Equal(t, "", f.Suffix()) require.Equal(t, "", f.Suffix())
} }
@ -146,28 +154,24 @@ func TestDecodeTypes(t *testing.T) {
json, found := tt.GetBySuffix("jasn") json, found := tt.GetBySuffix("jasn")
require.True(t, found) require.True(t, found)
require.Equal(t, "application/json", json.String(), name) require.Equal(t, "application/json", json.String(), name)
require.Equal(t, ".jasn", json.FullSuffix())
}}, }},
{ {
"Suffix from key, multiple file suffixes", "MIME suffix in key, multiple file suffixes, custom delimiter",
[]map[string]interface{}{ []map[string]interface{}{
{ {
"application/hugo+hg": map[string]interface{}{ "application/hugo+hg": map[string]interface{}{
"Suffixes": []string{"hg1", "hg2"}, "suffixes": []string{"hg1", "hg2"},
"Delimiter": "_",
}}}, }}},
false, false,
func(t *testing.T, name string, tt Types) { func(t *testing.T, name string, tt Types) {
require.Len(t, tt, len(DefaultTypes)+1) require.Len(t, tt, len(DefaultTypes)+1)
hg, found := tt.GetBySuffix("hg") hg, found := tt.GetBySuffix("hg2")
require.True(t, found) require.True(t, found)
require.Equal(t, "hg", hg.OldSuffix) require.Equal(t, "hg", hg.mimeSuffix)
require.Equal(t, "hg", hg.Suffix())
require.Equal(t, ".hg", hg.FullSuffix())
require.Equal(t, "application/hugo+hg", hg.String(), name)
hg, found = tt.GetBySuffix("hg2")
require.True(t, found)
require.Equal(t, "hg", hg.OldSuffix)
require.Equal(t, "hg2", hg.Suffix()) require.Equal(t, "hg2", hg.Suffix())
require.Equal(t, ".hg2", hg.FullSuffix()) require.Equal(t, "_hg2", hg.FullSuffix())
require.Equal(t, "application/hugo+hg", hg.String(), name) require.Equal(t, "application/hugo+hg", hg.String(), name)
hg, found = tt.GetByType("application/hugo+hg") hg, found = tt.GetByType("application/hugo+hg")
@ -178,8 +182,8 @@ func TestDecodeTypes(t *testing.T) {
"Add custom media type", "Add custom media type",
[]map[string]interface{}{ []map[string]interface{}{
{ {
"text/hugo": map[string]interface{}{ "text/hugo+hgo": map[string]interface{}{
"suffix": "hgo"}}}, "Suffixes": []string{"hgo2"}}}},
false, false,
func(t *testing.T, name string, tt Types) { func(t *testing.T, name string, tt Types) {
require.Len(t, tt, len(DefaultTypes)+1) require.Len(t, tt, len(DefaultTypes)+1)
@ -188,7 +192,7 @@ func TestDecodeTypes(t *testing.T) {
_, found := tt.GetBySuffix("json") _, found := tt.GetBySuffix("json")
require.True(t, found) require.True(t, found)
hugo, found := tt.GetBySuffix("hgo") hugo, found := tt.GetBySuffix("hgo2")
require.True(t, found) require.True(t, found)
require.Equal(t, "text/hugo+hgo", hugo.String(), name) require.Equal(t, "text/hugo+hgo", hugo.String(), name)
}}, }},

View file

@ -71,60 +71,35 @@ func New(mediaTypes media.Types, outputFormats output.Formats) Client {
} }
// We use the Type definition of the media types defined in the site if found. // We use the Type definition of the media types defined in the site if found.
addMinifierFunc(m, mediaTypes, "text/css", "css", css.Minify) addMinifierFunc(m, mediaTypes, "css", css.Minify)
addMinifierFunc(m, mediaTypes, "application/javascript", "js", js.Minify) addMinifierFunc(m, mediaTypes, "js", js.Minify)
m.AddFuncRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), js.Minify) m.AddFuncRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), js.Minify)
addMinifierFunc(m, mediaTypes, "application/json", "json", json.Minify) addMinifierFunc(m, mediaTypes, "json", json.Minify)
addMinifierFunc(m, mediaTypes, "image/svg+xml", "svg", svg.Minify) addMinifierFunc(m, mediaTypes, "svg", svg.Minify)
addMinifierFunc(m, mediaTypes, "application/xml", "xml", xml.Minify) addMinifierFunc(m, mediaTypes, "xml", xml.Minify)
addMinifierFunc(m, mediaTypes, "application/rss", "xml", xml.Minify)
// HTML // HTML
addMinifier(m, mediaTypes, "text/html", "html", htmlMin) addMinifier(m, mediaTypes, "html", htmlMin)
for _, of := range outputFormats { for _, of := range outputFormats {
if of.IsHTML { if of.IsHTML {
addMinifier(m, mediaTypes, of.MediaType.Type(), "html", htmlMin) m.Add(of.MediaType.Type(), htmlMin)
} }
} }
return Client{m: m} return Client{m: m}
} }
func addMinifier(m *minify.M, mt media.Types, typeString, suffix string, min minify.Minifier) { func addMinifier(m *minify.M, mt media.Types, suffix string, min minify.Minifier) {
resolvedTypeStr := resolveMediaTypeString(mt, typeString, suffix) types := mt.BySuffix(suffix)
m.Add(resolvedTypeStr, min) for _, t := range types {
if resolvedTypeStr != typeString { m.Add(t.Type(), min)
m.Add(typeString, min)
} }
} }
func addMinifierFunc(m *minify.M, mt media.Types, typeString, suffix string, fn minify.MinifierFunc) { func addMinifierFunc(m *minify.M, mt media.Types, suffix string, min minify.MinifierFunc) {
resolvedTypeStr := resolveMediaTypeString(mt, typeString, suffix) types := mt.BySuffix(suffix)
m.AddFunc(resolvedTypeStr, fn) for _, t := range types {
if resolvedTypeStr != typeString { m.AddFunc(t.Type(), min)
m.AddFunc(typeString, fn)
} }
} }
func resolveMediaTypeString(types media.Types, typeStr, suffix string) string {
if m, found := resolveMediaType(types, typeStr, suffix); found {
return m.Type()
}
// Fall back to the default.
return typeStr
}
// Make sure we match the matching pattern with what the user have actually defined
// in his or hers media types configuration.
func resolveMediaType(types media.Types, typeStr, suffix string) (media.Type, bool) {
if m, found := types.GetByType(typeStr); found {
return m, true
}
if m, found := types.GetFirstBySuffix(suffix); found {
return m, true
}
return media.Type{}, false
}

View file

@ -32,4 +32,10 @@ func TestNew(t *testing.T) {
assert.NoError(m.Minify(media.CSSType, &b, strings.NewReader("body { color: blue; }"))) assert.NoError(m.Minify(media.CSSType, &b, strings.NewReader("body { color: blue; }")))
assert.Equal("body{color:blue}", b.String()) assert.Equal("body{color:blue}", b.String())
b.Reset()
// RSS should be handled as XML
assert.NoError(m.Minify(media.RSSType, &b, strings.NewReader("<hello> Hugo! </hello> ")))
assert.Equal("<hello>Hugo!</hello>", b.String())
} }

View file

@ -93,11 +93,9 @@ func TestGetFormatByExt(t *testing.T) {
func TestGetFormatByFilename(t *testing.T) { func TestGetFormatByFilename(t *testing.T) {
noExtNoDelimMediaType := media.TextType noExtNoDelimMediaType := media.TextType
noExtNoDelimMediaType.OldSuffix = ""
noExtNoDelimMediaType.Delimiter = "" noExtNoDelimMediaType.Delimiter = ""
noExtMediaType := media.TextType noExtMediaType := media.TextType
noExtMediaType.OldSuffix = ""
var ( var (
noExtDelimFormat = Format{ noExtDelimFormat = Format{