diff --git a/hugolib/config_test.go b/hugolib/config_test.go index aec67336..2fa26d4f 100644 --- a/hugolib/config_test.go +++ b/hugolib/config_test.go @@ -219,8 +219,11 @@ map[string]interface {}{ "mediatype": Type{ MainType: "text", SubType: "m1", - Suffix: "m1main", + OldSuffix: "m1main", Delimiter: ".", + Suffixes: []string{ + "m1main", + }, }, }, "o2": map[string]interface {}{ @@ -228,8 +231,11 @@ map[string]interface {}{ "mediatype": Type{ MainType: "text", SubType: "m2", - Suffix: "m2theme", + OldSuffix: "m2theme", Delimiter: ".", + Suffixes: []string{ + "m2theme", + }, }, }, }`, got["outputformats"]) diff --git a/hugolib/page_output.go b/hugolib/page_output.go index 6fffbae8..204f5ace 100644 --- a/hugolib/page_output.go +++ b/hugolib/page_output.go @@ -218,7 +218,7 @@ func newOutputFormat(p *Page, f output.Format) *OutputFormat { func (p *PageOutput) AlternativeOutputFormats() (OutputFormats, error) { var o OutputFormats for _, of := range p.OutputFormats() { - if of.f.NotAlternative || of.f == p.outputFormat { + if of.f.NotAlternative || of.f.Name == p.outputFormat.Name { continue } o = append(o, of) diff --git a/hugolib/page_paths.go b/hugolib/page_paths.go index 1b2d00ad..550df130 100644 --- a/hugolib/page_paths.go +++ b/hugolib/page_paths.go @@ -239,7 +239,7 @@ func createTargetPath(d targetPathDescriptor) string { } if isUgly { - pagePath += d.Type.MediaType.Delimiter + d.Type.MediaType.Suffix + pagePath += d.Type.MediaType.FullSuffix() } else { pagePath = filepath.Join(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix()) } diff --git a/hugolib/page_paths_test.go b/hugolib/page_paths_test.go index 3ca500f1..8f8df6ec 100644 --- a/hugolib/page_paths_test.go +++ b/hugolib/page_paths_test.go @@ -30,7 +30,7 @@ func TestPageTargetPath(t *testing.T) { pathSpec := newTestDefaultPathSpec(t) noExtNoDelimMediaType := media.TextType - noExtNoDelimMediaType.Suffix = "" + noExtNoDelimMediaType.Suffixes = []string{} noExtNoDelimMediaType.Delimiter = "" // Netlify style _redirects @@ -169,8 +169,8 @@ func TestPageTargetPath(t *testing.T) { } else if test.d.Kind == KindHome && test.d.Type.Path != "" { } else if (!strings.HasPrefix(expected, "/index") || test.d.Addends != "") && test.d.URL == "" && isUgly { expected = strings.Replace(expected, - "/"+test.d.Type.BaseName+"."+test.d.Type.MediaType.Suffix, - "."+test.d.Type.MediaType.Suffix, -1) + "/"+test.d.Type.BaseName+"."+test.d.Type.MediaType.Suffix(), + "."+test.d.Type.MediaType.Suffix(), -1) } if test.d.LangPrefix != "" && !(test.d.Kind == KindPage && test.d.URL != "") { diff --git a/hugolib/pagination_test.go b/hugolib/pagination_test.go index 94f7301b..5dbef609 100644 --- a/hugolib/pagination_test.go +++ b/hugolib/pagination_test.go @@ -239,7 +239,7 @@ func TestPaginationURLFactory(t *testing.T) { } if uglyURLs { - expected = expected[:len(expected)-1] + "." + test.d.Type.MediaType.Suffix + expected = expected[:len(expected)-1] + "." + test.d.Type.MediaType.Suffix() } pathSpec := newTestPathSpec(fs, cfg) diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index cf5b0ece..27529377 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -173,11 +173,11 @@ type scKey struct { } func newScKey(m media.Type, shortcodeplaceholder string) scKey { - return scKey{Suffix: m.Suffix, ShortcodePlaceholder: shortcodeplaceholder} + return scKey{Suffix: m.Suffix(), ShortcodePlaceholder: shortcodeplaceholder} } func newScKeyFromLangAndOutputFormat(lang string, o output.Format, shortcodeplaceholder string) scKey { - return scKey{Lang: lang, Suffix: o.MediaType.Suffix, OutputFormat: o.Name, ShortcodePlaceholder: shortcodeplaceholder} + return scKey{Lang: lang, Suffix: o.MediaType.Suffix(), OutputFormat: o.Name, ShortcodePlaceholder: shortcodeplaceholder} } func newDefaultScKey(shortcodeplaceholder string) scKey { diff --git a/hugolib/site.go b/hugolib/site.go index a749bafd..5e300393 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -586,8 +586,9 @@ type whatChanged struct { // package, so it will behave correctly with Hugo's built-in server. func (s *Site) RegisterMediaTypes() { for _, mt := range s.mediaTypesConfig { - // The last one will win if there are any duplicates. - _ = mime.AddExtensionType("."+mt.Suffix, mt.Type()+"; charset=utf-8") + 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 5f864538..0677dfbf 100644 --- a/hugolib/site_output_test.go +++ b/hugolib/site_output_test.go @@ -179,7 +179,7 @@ Len Pages: {{ .Kind }} {{ len .Site.RegularPages }} Page Number: {{ .Paginator.P th.assertFileContent("public/index.html", // The HTML entity is a deliberate part of this test: The HTML templates are // parsed with html/template. - `List HTML|JSON Home|`, + `List HTML|JSON Home|`, "en: Elbow", "ShortHTML", "OtherShort:

Hi!

", @@ -195,7 +195,7 @@ Len Pages: {{ .Kind }} {{ len .Site.RegularPages }} Page Number: {{ .Paginator.P th.assertFileContent("public/index.json", "Output/Rel: JSON/canonical|", // JSON is plain text, so no need to safeHTML this and that - ``, + ``, "ShortJSON", "OtherShort:

Hi!

", ) diff --git a/hugolib/site_render.go b/hugolib/site_render.go index d6b7a76f..2da4064b 100644 --- a/hugolib/site_render.go +++ b/hugolib/site_render.go @@ -69,7 +69,7 @@ func headlessPagesPublisher(s *Site, wg *sync.WaitGroup) { defer wg.Done() for _, page := range s.headlessPages { outFormat := page.outputFormats[0] // There is only one - if outFormat != s.rc.Format { + if outFormat.Name != s.rc.Format.Name { // Avoid double work. continue } @@ -92,7 +92,7 @@ func pageRenderer(s *Site, pages <-chan *Page, results chan<- error, wg *sync.Wa for i, outFormat := range page.outputFormats { - if outFormat != page.s.rc.Format { + if outFormat.Name != page.s.rc.Format.Name { // Will be rendered ... later. continue } diff --git a/media/mediaType.go b/media/mediaType.go index 07ba410f..f26209ea 100644 --- a/media/mediaType.go +++ b/media/mediaType.go @@ -19,6 +19,7 @@ import ( "sort" "strings" + "github.com/gohugoio/hugo/helpers" "github.com/mitchellh/mapstructure" ) @@ -29,19 +30,39 @@ const ( // Type (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 image/jpeg+jpg +// 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 type Type struct { - MainType string `json:"mainType"` // i.e. text - SubType string `json:"subType"` // i.e. html - Suffix string `json:"suffix"` // i.e html - Delimiter string `json:"delimiter"` // defaults to "." + MainType string `json:"mainType"` // i.e. text + SubType string `json:"subType"` // i.e. html + + // Deprecated in Hugo 0.44. To be renamed and unexported. + // Was earlier used both to set file suffix and to augment the MIME type. + // This had its limitations and issues. + OldSuffix string `json:"-" mapstructure:"suffix"` + + Delimiter string `json:"delimiter"` // e.g. "." + + Suffixes []string `json:"suffixes"` + + // Set when doing lookup by suffix. + fileSuffix string } -// FromString creates a new Type given a type sring on the form MainType/SubType and +// FromStringAndExt is same as FromString, but adds the file extension to the type. +func FromStringAndExt(t, ext string) (Type, error) { + tp, err := fromString(t) + if err != nil { + return tp, err + } + tp.Suffixes = []string{strings.TrimPrefix(ext, ".")} + return tp, nil +} + +// 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) { +func fromString(t string) (Type, error) { t = strings.ToLower(t) parts := strings.Split(t, "/") if len(parts) != 2 { @@ -54,54 +75,67 @@ func FromString(t string) (Type, error) { var suffix string - if len(subParts) == 1 { - suffix = subType - } else { + if len(subParts) > 1 { suffix = subParts[1] } - return Type{MainType: mainType, SubType: subType, Suffix: suffix, Delimiter: defaultDelimiter}, nil + return Type{MainType: mainType, SubType: subType, OldSuffix: suffix}, nil } -// Type returns a string representing the main- and sub-type of a media type, i.e. "text/css". +// 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.OldSuffix != "" { + return fmt.Sprintf("%s/%s+%s", m.MainType, m.SubType, m.OldSuffix) + } return fmt.Sprintf("%s/%s", m.MainType, m.SubType) + } func (m Type) String() string { - if m.Suffix != "" { - return fmt.Sprintf("%s/%s+%s", m.MainType, m.SubType, m.Suffix) - } - return fmt.Sprintf("%s/%s", m.MainType, m.SubType) + return m.Type() } // FullSuffix returns the file suffix with any delimiter prepended. func (m Type) FullSuffix() string { - return m.Delimiter + m.Suffix + return m.Delimiter + m.Suffix() +} + +// Suffix returns the file suffix without any delmiter prepended. +func (m Type) Suffix() string { + if m.fileSuffix != "" { + return m.fileSuffix + } + if len(m.Suffixes) > 0 { + return m.Suffixes[0] + } + // There are MIME types without file suffixes. + return "" } var ( - CalendarType = Type{"text", "calendar", "ics", defaultDelimiter} - CSSType = Type{"text", "css", "css", defaultDelimiter} - SCSSType = Type{"text", "x-scss", "scss", defaultDelimiter} - SASSType = Type{"text", "x-sass", "sass", defaultDelimiter} - CSVType = Type{"text", "csv", "csv", defaultDelimiter} - HTMLType = Type{"text", "html", "html", defaultDelimiter} - JavascriptType = Type{"application", "javascript", "js", defaultDelimiter} - JSONType = Type{"application", "json", "json", defaultDelimiter} - RSSType = Type{"application", "rss", "xml", defaultDelimiter} - XMLType = Type{"application", "xml", "xml", defaultDelimiter} - // The official MIME type of SVG is image/svg+xml. We currently only support one extension - // per mime type. The workaround in projects is to create multiple media type definitions, - // but we need to improve this to take other known suffixes into account. - // But until then, svg has an svg extension, which is very common. TODO(bep) - SVGType = Type{"image", "svg", "svg", defaultDelimiter} - TextType = Type{"text", "plain", "txt", defaultDelimiter} + // 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. + 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} + JSONType = Type{MainType: "application", SubType: "json", Suffixes: []string{"json"}, Delimiter: defaultDelimiter} + RSSType = Type{MainType: "application", SubType: "rss", OldSuffix: "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} + TextType = Type{MainType: "text", SubType: "plain", Suffixes: []string{"txt"}, Delimiter: defaultDelimiter} - OctetType = Type{"application", "octet-stream", "", ""} + OctetType = Type{MainType: "application", SubType: "octet-stream"} ) var DefaultTypes = Types{ @@ -136,13 +170,23 @@ func (t Types) GetByType(tp string) (Type, bool) { return tt, true } } + + if !strings.Contains(tp, "+") { + // Try with the main and sub type + parts := strings.Split(tp, "/") + if len(parts) == 2 { + return t.GetByMainSubType(parts[0], parts[1]) + } + } + return Type{}, false } // GetFirstBySuffix will return the first media type matching the given suffix. func (t Types) GetFirstBySuffix(suffix string) (Type, bool) { for _, tt := range t { - if strings.EqualFold(suffix, tt.Suffix) { + if match := tt.matchSuffix(suffix); match != "" { + tt.fileSuffix = match return tt, true } } @@ -155,12 +199,46 @@ func (t Types) GetFirstBySuffix(suffix string) (Type, bool) { // The lookup is case insensitive. func (t Types) GetBySuffix(suffix string) (tp Type, found bool) { for _, tt := range t { - if strings.EqualFold(suffix, tt.Suffix) { + if match := tt.matchSuffix(suffix); match != "" { if found { // ambiguous found = false return } + tp = tt + tp.fileSuffix = match + found = true + } + } + return +} + +func (t Type) matchSuffix(suffix string) string { + if strings.EqualFold(suffix, t.OldSuffix) { + return t.OldSuffix + } + for _, s := range t.Suffixes { + if strings.EqualFold(suffix, s) { + return s + } + } + + return "" +} + +// GetMainSubType gets a media type given a main and a sub type e.g. "text" and "plain". +// It will return false if no format could be found, or if the combination given +// is ambiguous. +// The lookup is case insensitive. +func (t Types) GetByMainSubType(mainType, subType string) (tp Type, found bool) { + for _, tt := range t { + if strings.EqualFold(mainType, tt.MainType) && strings.EqualFold(subType, tt.SubType) { + if found { + // ambiguous + found = false + return + } + tp = tt found = true } @@ -168,46 +246,99 @@ func (t Types) GetBySuffix(suffix string) (tp Type, found bool) { return } +func suffixIsDeprecated() { + helpers.Deprecated("MediaType", "Suffix in config.toml", ` +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"] + +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. +`, false) +} + // 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(maps ...map[string]interface{}) (Types, error) { - m := make(Types, len(DefaultTypes)) - copy(m, DefaultTypes) + 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 { + suffixes := make([]string, len(dt.Suffixes)) + copy(suffixes, dt.Suffixes) + dt.Suffixes = suffixes + mmm[dt.Type()] = dt + } for _, mm := range maps { for k, v := range mm { - // It may be tempting to put the full media type in the key, e.g. - // "text/css+css", but that will break the logic below. - if strings.Contains(k, "+") { - return Types{}, fmt.Errorf("media type keys cannot contain any '+' chars. Valid example is %q", "text/css") - } + var mediaType Type - found := false - for i, vv := range m { - // Match by type, i.e. "text/css" - if strings.EqualFold(k, vv.Type()) { - // Merge it with the existing - if err := mapstructure.WeakDecode(v, &m[i]); err != nil { - return m, err - } - found = true - } - } + mediaType, found := mmm[k] if !found { - mediaType, err := FromString(k) + var err error + mediaType, err = fromString(k) if err != nil { return m, err } - - if err := mapstructure.WeakDecode(v, &mediaType); err != nil { - return m, err - } - - m = append(m, mediaType) } + + if err := mapstructure.WeakDecode(v, &mediaType); err != nil { + return m, err + } + + vm := v.(map[string]interface{}) + _, delimiterSet := vm["delimiter"] + _, suffixSet := vm["suffix"] + + if mediaType.OldSuffix != "" { + suffixIsDeprecated() + } + + // 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. + if !delimiterSet && len(mediaType.Suffixes) != 0 { + mediaType.Delimiter = defaultDelimiter + } else if suffixSet && !delimiterSet { + mediaType.Delimiter = defaultDelimiter + } + + mmm[k] = mediaType + } } + for _, v := range mmm { + m = append(m, v) + } sort.Sort(m) return m, nil diff --git a/media/mediaType_test.go b/media/mediaType_test.go index f3ddb086..6385528e 100644 --- a/media/mediaType_test.go +++ b/media/mediaType_test.go @@ -28,21 +28,21 @@ func TestDefaultTypes(t *testing.T) { expectedType string expectedString string }{ - {CalendarType, "text", "calendar", "ics", "text/calendar", "text/calendar+ics"}, - {CSSType, "text", "css", "css", "text/css", "text/css+css"}, - {SCSSType, "text", "x-scss", "scss", "text/x-scss", "text/x-scss+scss"}, - {CSVType, "text", "csv", "csv", "text/csv", "text/csv+csv"}, - {HTMLType, "text", "html", "html", "text/html", "text/html+html"}, - {JavascriptType, "application", "javascript", "js", "application/javascript", "application/javascript+js"}, - {JSONType, "application", "json", "json", "application/json", "application/json+json"}, - {RSSType, "application", "rss", "xml", "application/rss", "application/rss+xml"}, - {SVGType, "image", "svg", "svg", "image/svg", "image/svg+svg"}, - {TextType, "text", "plain", "txt", "text/plain", "text/plain+txt"}, - {XMLType, "application", "xml", "xml", "application/xml", "application/xml+xml"}, + {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, "application", "javascript", "js", "application/javascript", "application/javascript"}, + {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"}, } { require.Equal(t, test.expectedMainType, test.tp.MainType) require.Equal(t, test.expectedSubType, test.tp.SubType) - require.Equal(t, test.expectedSuffix, test.tp.Suffix) + require.Equal(t, test.expectedSuffix, test.tp.Suffix(), test.tp.String()) require.Equal(t, defaultDelimiter, test.tp.Delimiter) require.Equal(t, test.expectedType, test.tp.Type()) @@ -61,34 +61,68 @@ func TestGetByType(t *testing.T) { _, found = types.GetByType("text/nono") require.False(t, found) + + mt, found = types.GetByType("application/rss+xml") + require.True(t, found) + require.Equal(t, mt, RSSType) + + mt, found = types.GetByType("application/rss") + require.True(t, found) + require.Equal(t, mt, RSSType) +} + +func TestGetByMainSubType(t *testing.T) { + assert := require.New(t) + f, found := DefaultTypes.GetByMainSubType("text", "plain") + assert.True(found) + assert.Equal(f, TextType) + _, found = DefaultTypes.GetByMainSubType("foo", "plain") + assert.False(found) } func TestGetFirstBySuffix(t *testing.T) { assert := require.New(t) f, found := DefaultTypes.GetFirstBySuffix("xml") assert.True(found) - assert.Equal(Type{MainType: "application", SubType: "rss", Suffix: "xml", Delimiter: "."}, f) + assert.Equal(Type{MainType: "application", SubType: "rss", OldSuffix: "xml", Delimiter: ".", Suffixes: []string{"xml"}, fileSuffix: "xml"}, f) } func TestFromTypeString(t *testing.T) { - f, err := FromString("text/html") + f, err := fromString("text/html") require.NoError(t, err) - require.Equal(t, HTMLType, f) + require.Equal(t, HTMLType.Type(), f.Type()) - f, err = FromString("application/custom") + f, err = fromString("application/custom") require.NoError(t, err) - require.Equal(t, Type{MainType: "application", SubType: "custom", Suffix: "custom", Delimiter: defaultDelimiter}, f) + require.Equal(t, Type{MainType: "application", SubType: "custom", OldSuffix: "", fileSuffix: ""}, f) - f, err = FromString("application/custom+pdf") + f, err = fromString("application/custom+sfx") require.NoError(t, err) - require.Equal(t, Type{MainType: "application", SubType: "custom", Suffix: "pdf", Delimiter: defaultDelimiter}, f) + require.Equal(t, Type{MainType: "application", SubType: "custom", OldSuffix: "sfx"}, f) - _, err = FromString("noslash") + _, err = fromString("noslash") 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.Equal(t, Type{MainType: "text", SubType: "xml", Suffix: "xml", Delimiter: "."}, f) + require.Equal(t, Type{MainType: "text", SubType: "xml", OldSuffix: ""}, f) + require.Equal(t, "", f.Suffix()) +} + +// Add a test for the SVG case +// https://github.com/gohugoio/hugo/issues/4920 +func TestFromExtensionMultipleSuffixes(t *testing.T) { + assert := require.New(t) + tp, found := DefaultTypes.GetBySuffix("svg") + assert.True(found) + assert.Equal("image/svg+xml", tp.String()) + assert.Equal("svg", tp.fileSuffix) + assert.Equal(".svg", tp.FullSuffix()) + tp, found = DefaultTypes.GetByType("image/svg+xml") + assert.True(found) + assert.Equal("image/svg+xml", tp.String()) + assert.True(found) + assert.Equal(".svg", tp.FullSuffix()) } @@ -105,13 +139,40 @@ func TestDecodeTypes(t *testing.T) { []map[string]interface{}{ { "application/json": map[string]interface{}{ - "suffix": "jsn"}}}, + "suffixes": []string{"jasn"}}}}, false, func(t *testing.T, name string, tt Types) { require.Len(t, tt, len(DefaultTypes)) - json, found := tt.GetBySuffix("jsn") + json, found := tt.GetBySuffix("jasn") require.True(t, found) - require.Equal(t, "application/json+jsn", json.String(), name) + require.Equal(t, "application/json", json.String(), name) + }}, + { + "Suffix from key, multiple file suffixes", + []map[string]interface{}{ + { + "application/hugo+hg": map[string]interface{}{ + "Suffixes": []string{"hg1", "hg2"}, + }}}, + false, + func(t *testing.T, name string, tt Types) { + require.Len(t, tt, len(DefaultTypes)+1) + hg, found := tt.GetBySuffix("hg") + require.True(t, found) + require.Equal(t, "hg", hg.OldSuffix) + 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.FullSuffix()) + require.Equal(t, "application/hugo+hg", hg.String(), name) + + hg, found = tt.GetByType("application/hugo+hg") + require.True(t, found) + }}, { "Add custom media type", @@ -123,6 +184,7 @@ func TestDecodeTypes(t *testing.T) { func(t *testing.T, name string, tt Types) { require.Len(t, tt, len(DefaultTypes)+1) // Make sure we have not broken the default config. + _, found := tt.GetBySuffix("json") require.True(t, found) @@ -130,15 +192,6 @@ func TestDecodeTypes(t *testing.T) { require.True(t, found) require.Equal(t, "text/hugo+hgo", hugo.String(), name) }}, - { - "Add media type invalid key", - []map[string]interface{}{ - { - "text/hugo+hgo": map[string]interface{}{}}}, - true, - func(t *testing.T, name string, tt Types) { - - }}, } for _, test := range tests { diff --git a/output/docshelper.go b/output/docshelper.go index 4c724b02..ad16d325 100644 --- a/output/docshelper.go +++ b/output/docshelper.go @@ -72,7 +72,7 @@ func createLayoutExamples() interface{} { Example: example.name, Kind: example.d.Kind, OutputFormat: example.f.Name, - Suffix: example.f.MediaType.Suffix, + Suffix: example.f.MediaType.Suffix(), Layouts: makeLayoutsPresentable(layouts)}) } diff --git a/output/layout.go b/output/layout.go index f83490d8..5d72938a 100644 --- a/output/layout.go +++ b/output/layout.go @@ -47,7 +47,7 @@ type LayoutHandler struct { type layoutCacheKey struct { d LayoutDescriptor - f Format + f string } // NewLayoutHandler creates a new LayoutHandler. @@ -60,7 +60,7 @@ func NewLayoutHandler() *LayoutHandler { func (l *LayoutHandler) For(d LayoutDescriptor, f Format) ([]string, error) { // We will get lots of requests for the same layouts, so avoid recalculations. - key := layoutCacheKey{d, f} + key := layoutCacheKey{d, f.Name} l.mu.RLock() if cacheVal, found := l.cache[key]; found { l.mu.RUnlock() @@ -209,7 +209,7 @@ func (l *layoutBuilder) resolveVariations() []string { "TYPE", typeVar, "LAYOUT", layoutVar, "VARIATIONS", variation, - "EXTENSION", l.f.MediaType.Suffix, + "EXTENSION", l.f.MediaType.Suffix(), )) } } diff --git a/output/layout_test.go b/output/layout_test.go index 4b958e9f..e5f2b5b6 100644 --- a/output/layout_test.go +++ b/output/layout_test.go @@ -27,11 +27,11 @@ import ( func TestLayout(t *testing.T) { noExtNoDelimMediaType := media.TextType - noExtNoDelimMediaType.Suffix = "" + noExtNoDelimMediaType.Suffixes = nil noExtNoDelimMediaType.Delimiter = "" noExtMediaType := media.TextType - noExtMediaType.Suffix = "" + noExtMediaType.Suffixes = nil var ( ampType = Format{ @@ -47,6 +47,7 @@ func TestLayout(t *testing.T) { MediaType: noExtNoDelimMediaType, BaseName: "_redirects", } + noExt = Format{ Name: "NEX", MediaType: noExtMediaType, diff --git a/output/outputFormat.go b/output/outputFormat.go index 87785016..30bf903b 100644 --- a/output/outputFormat.go +++ b/output/outputFormat.go @@ -178,7 +178,7 @@ func (formats Formats) Less(i, j int) bool { return formats[i].Name < formats[j] // 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 strings.EqualFold(suffix, ff.MediaType.Suffix()) { if found { // ambiguous found = false @@ -331,7 +331,7 @@ func decode(mediaTypes media.Types, input, output interface{}) error { } func (formats Format) BaseFilename() string { - return formats.BaseName + "." + formats.MediaType.Suffix + return formats.BaseName + formats.MediaType.FullSuffix() } func (formats Format) MarshalJSON() ([]byte, error) { diff --git a/output/outputFormat_test.go b/output/outputFormat_test.go index b800d1a3..5d0620fa 100644 --- a/output/outputFormat_test.go +++ b/output/outputFormat_test.go @@ -93,11 +93,11 @@ func TestGetFormatByExt(t *testing.T) { func TestGetFormatByFilename(t *testing.T) { noExtNoDelimMediaType := media.TextType - noExtNoDelimMediaType.Suffix = "" + noExtNoDelimMediaType.OldSuffix = "" noExtNoDelimMediaType.Delimiter = "" noExtMediaType := media.TextType - noExtMediaType.Suffix = "" + noExtMediaType.OldSuffix = "" var ( noExtDelimFormat = Format{ diff --git a/resource/bundler/bundler.go b/resource/bundler/bundler.go index 2f398148..a86b39ef 100644 --- a/resource/bundler/bundler.go +++ b/resource/bundler/bundler.go @@ -70,7 +70,7 @@ func (c *Client) Concat(targetPath string, resources []resource.Resource) (resou // The given set of resources must be of the same Media Type. // We may improve on that in the future, but then we need to know more. for i, r := range resources { - if i > 0 && r.MediaType() != resolvedm { + if i > 0 && r.MediaType().Type() != resolvedm.Type() { return nil, errors.New("resources in Concat must be of the same Media Type") } resolvedm = r.MediaType() diff --git a/resource/minifiers/minify.go b/resource/minifiers/minify.go index 609b9a69..604ac6f8 100644 --- a/resource/minifiers/minify.go +++ b/resource/minifiers/minify.go @@ -45,7 +45,7 @@ func New(rs *resource.Spec) *Client { addMinifierFunc(m, mt, "text/html", "html", html.Minify) addMinifierFunc(m, mt, "application/javascript", "js", js.Minify) addMinifierFunc(m, mt, "application/json", "json", json.Minify) - addMinifierFunc(m, mt, "image/svg", "xml", svg.Minify) + addMinifierFunc(m, mt, "image/svg+xml", "svg", svg.Minify) addMinifierFunc(m, mt, "application/xml", "xml", xml.Minify) addMinifierFunc(m, mt, "application/rss", "xml", xml.Minify) diff --git a/resource/resource.go b/resource/resource.go index f0989e51..a7a9cb87 100644 --- a/resource/resource.go +++ b/resource/resource.go @@ -416,16 +416,15 @@ func (r *Spec) newResource(sourceFs afero.Fs, fd ResourceSourceDescriptor) (Reso mimeType, found := r.MediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, ".")) // TODO(bep) we need to handle these ambigous types better, but in this context // we most likely want the application/xml type. - if mimeType.Suffix == "xml" && mimeType.SubType == "rss" { + if mimeType.Suffix() == "xml" && mimeType.SubType == "rss" { mimeType, found = r.MediaTypes.GetByType("application/xml") } if !found { mimeStr := mime.TypeByExtension(ext) if mimeStr != "" { - mimeType, _ = media.FromString(mimeStr) + mimeType, _ = media.FromStringAndExt(mimeStr, ext) } - } gr := r.newGenericResourceWithBase( diff --git a/resource/resource_test.go b/resource/resource_test.go index 659994c3..e699e6f3 100644 --- a/resource/resource_test.go +++ b/resource/resource_test.go @@ -97,7 +97,7 @@ func TestNewResourceFromFilenameSubPathInBaseURL(t *testing.T) { } -var pngType, _ = media.FromString("image/png") +var pngType, _ = media.FromStringAndExt("image/png", "png") func TestResourcesByType(t *testing.T) { assert := require.New(t)