Create a struct with all of Hugo's config options

Primary motivation is documentation, but it will also hopefully simplify the code.

Also,

* Lower case the default output format names; this is in line with the custom ones (map keys) and how
it's treated all the places. This avoids doing `stringds.EqualFold` everywhere.

Closes #10896
Closes #10620
This commit is contained in:
Bjørn Erik Pedersen
2023-01-04 18:24:36 +01:00
parent 6aededf6b4
commit 241b21b0fd
337 changed files with 13377 additions and 14898 deletions

147
output/config.go Normal file
View File

@@ -0,0 +1,147 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package output
import (
"fmt"
"reflect"
"sort"
"strings"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/media"
"github.com/mitchellh/mapstructure"
)
// OutputFormatConfig configures a single output format.
type OutputFormatConfig struct {
// The MediaType string. This must be a configured media type.
MediaType string
Format
}
func DecodeConfig(mediaTypes media.Types, in any) (*config.ConfigNamespace[map[string]OutputFormatConfig, Formats], error) {
buildConfig := func(in any) (Formats, any, error) {
f := make(Formats, len(DefaultFormats))
copy(f, DefaultFormats)
if in != nil {
m, err := maps.ToStringMapE(in)
if err != nil {
return nil, nil, fmt.Errorf("failed convert config to map: %s", err)
}
m = maps.CleanConfigStringMap(m)
for k, v := range m {
found := false
for i, vv := range f {
// Both are lower case.
if k == vv.Name {
// Merge it with the existing
if err := decode(mediaTypes, v, &f[i]); err != nil {
return f, nil, err
}
found = true
}
}
if found {
continue
}
var newOutFormat Format
newOutFormat.Name = k
if err := decode(mediaTypes, v, &newOutFormat); err != nil {
return f, nil, err
}
// We need values for these
if newOutFormat.BaseName == "" {
newOutFormat.BaseName = "index"
}
if newOutFormat.Rel == "" {
newOutFormat.Rel = "alternate"
}
f = append(f, newOutFormat)
}
}
// Also format is a map for documentation purposes.
docm := make(map[string]OutputFormatConfig, len(f))
for _, ff := range f {
docm[ff.Name] = OutputFormatConfig{
MediaType: ff.MediaType.Type,
Format: ff,
}
}
sort.Sort(f)
return f, docm, nil
}
return config.DecodeNamespace[map[string]OutputFormatConfig](in, buildConfig)
}
func decode(mediaTypes media.Types, input any, output *Format) error {
config := &mapstructure.DecoderConfig{
Metadata: nil,
Result: output,
WeaklyTypedInput: true,
DecodeHook: func(a reflect.Type, b reflect.Type, c any) (any, error) {
if a.Kind() == reflect.Map {
dataVal := reflect.Indirect(reflect.ValueOf(c))
for _, key := range dataVal.MapKeys() {
keyStr, ok := key.Interface().(string)
if !ok {
// Not a string key
continue
}
if strings.EqualFold(keyStr, "mediaType") {
// If mediaType is a string, look it up and replace it
// in the map.
vv := dataVal.MapIndex(key)
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", vviv)
}
dataVal.SetMapIndex(key, reflect.ValueOf(mediaType))
default:
return nil, fmt.Errorf("invalid output format configuration; wrong type for media type, expected string (e.g. text/html), got %T", vvi)
}
}
}
}
return c, nil
},
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
return err
}
if err = decoder.Decode(input); err != nil {
return fmt.Errorf("failed to decode output format configuration: %w", err)
}
return nil
}

98
output/config_test.go Normal file
View File

@@ -0,0 +1,98 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package output
import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/media"
)
func TestDecodeConfig(t *testing.T) {
c := qt.New(t)
mediaTypes := media.Types{media.Builtin.JSONType, media.Builtin.XMLType}
tests := []struct {
name string
m map[string]any
shouldError bool
assert func(t *testing.T, name string, f Formats)
}{
{
"Redefine JSON",
map[string]any{
"json": map[string]any{
"baseName": "myindex",
"isPlainText": "false",
},
},
false,
func(t *testing.T, name string, f Formats) {
msg := qt.Commentf(name)
c.Assert(len(f), qt.Equals, len(DefaultFormats), msg)
json, _ := f.GetByName("JSON")
c.Assert(json.BaseName, qt.Equals, "myindex")
c.Assert(json.MediaType, qt.Equals, media.Builtin.JSONType)
c.Assert(json.IsPlainText, qt.Equals, false)
},
},
{
"Add XML format with string as mediatype",
map[string]any{
"MYXMLFORMAT": map[string]any{
"baseName": "myxml",
"mediaType": "application/xml",
},
},
false,
func(t *testing.T, name string, f Formats) {
c.Assert(len(f), qt.Equals, len(DefaultFormats)+1)
xml, found := f.GetByName("MYXMLFORMAT")
c.Assert(found, qt.Equals, true)
c.Assert(xml.BaseName, qt.Equals, "myxml")
c.Assert(xml.MediaType, qt.Equals, media.Builtin.XMLType)
// Verify that we haven't changed the DefaultFormats slice.
json, _ := f.GetByName("JSON")
c.Assert(json.BaseName, qt.Equals, "index")
},
},
{
"Add format unknown mediatype",
map[string]any{
"MYINVALID": map[string]any{
"baseName": "mymy",
"mediaType": "application/hugo",
},
},
true,
func(t *testing.T, name string, f Formats) {
},
},
}
for _, test := range tests {
result, err := DecodeConfig(mediaTypes, test.m)
msg := qt.Commentf(test.name)
if test.shouldError {
c.Assert(err, qt.Not(qt.IsNil), msg)
} else {
c.Assert(err, qt.IsNil, msg)
test.assert(t, test.name, result.Config)
}
}
}

View File

@@ -6,6 +6,7 @@ import (
// "fmt"
"github.com/gohugoio/hugo/docshelper"
"github.com/gohugoio/hugo/output/layouts"
)
// This is is just some helpers used to create some JSON used in the Hugo docs.
@@ -39,44 +40,43 @@ func createLayoutExamples() any {
for _, example := range []struct {
name string
d LayoutDescriptor
f Format
d layouts.LayoutDescriptor
}{
// Taxonomy output.LayoutDescriptor={categories category taxonomy en false Type Section
{"Single page in \"posts\" section", LayoutDescriptor{Kind: "page", Type: "posts"}, HTMLFormat},
{"Base template for single page in \"posts\" section", LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts"}, HTMLFormat},
{"Single page in \"posts\" section with layout set", LayoutDescriptor{Kind: "page", Type: "posts", Layout: demoLayout}, HTMLFormat},
{"Base template for single page in \"posts\" section with layout set", LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", Layout: demoLayout}, HTMLFormat},
{"AMP single page", LayoutDescriptor{Kind: "page", Type: "posts"}, AMPFormat},
{"AMP single page, French language", LayoutDescriptor{Kind: "page", Type: "posts", Lang: "fr"}, AMPFormat},
// Taxonomy layouts.LayoutDescriptor={categories category taxonomy en false Type Section
{"Single page in \"posts\" section", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}},
{"Base template for single page in \"posts\" section", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}},
{"Single page in \"posts\" section with layout set", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
{"Base template for single page in \"posts\" section with layout set", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
{"AMP single page", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "amp", Suffix: "html"}},
{"AMP single page, French language", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Lang: "fr", OutputFormatName: "html", Suffix: "html"}},
// All section or typeless pages gets "page" as type
{"Home page", LayoutDescriptor{Kind: "home", Type: "page"}, HTMLFormat},
{"Base template for home page", LayoutDescriptor{Baseof: true, Kind: "home", Type: "page"}, HTMLFormat},
{"Home page with type set", LayoutDescriptor{Kind: "home", Type: demoType}, HTMLFormat},
{"Base template for home page with type set", LayoutDescriptor{Baseof: true, Kind: "home", Type: demoType}, HTMLFormat},
{"Home page with layout set", LayoutDescriptor{Kind: "home", Type: "page", Layout: demoLayout}, HTMLFormat},
{"AMP home, French language", LayoutDescriptor{Kind: "home", Type: "page", Lang: "fr"}, AMPFormat},
{"JSON home", LayoutDescriptor{Kind: "home", Type: "page"}, JSONFormat},
{"RSS home", LayoutDescriptor{Kind: "home", Type: "page"}, RSSFormat},
{"RSS section posts", LayoutDescriptor{Kind: "section", Type: "posts"}, RSSFormat},
{"Taxonomy in categories", LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category"}, RSSFormat},
{"Term in categories", LayoutDescriptor{Kind: "term", Type: "categories", Section: "category"}, RSSFormat},
{"Section list for \"posts\" section", LayoutDescriptor{Kind: "section", Type: "posts", Section: "posts"}, HTMLFormat},
{"Section list for \"posts\" section with type set to \"blog\"", LayoutDescriptor{Kind: "section", Type: "blog", Section: "posts"}, HTMLFormat},
{"Section list for \"posts\" section with layout set to \"demoLayout\"", LayoutDescriptor{Kind: "section", Layout: demoLayout, Section: "posts"}, HTMLFormat},
{"Home page", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
{"Base template for home page", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
{"Home page with type set", layouts.LayoutDescriptor{Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}},
{"Base template for home page with type set", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}},
{"Home page with layout set", layouts.LayoutDescriptor{Kind: "home", Type: "page", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
{"AMP home, French language", layouts.LayoutDescriptor{Kind: "home", Type: "page", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}},
{"JSON home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "json", Suffix: "json"}},
{"RSS home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "rss", Suffix: "rss"}},
{"RSS section posts", layouts.LayoutDescriptor{Kind: "section", Type: "posts", OutputFormatName: "rss", Suffix: "rss"}},
{"Taxonomy in categories", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "rss"}},
{"Term in categories", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "rss"}},
{"Section list for \"posts\" section", layouts.LayoutDescriptor{Kind: "section", Type: "posts", Section: "posts", OutputFormatName: "html", Suffix: "html"}},
{"Section list for \"posts\" section with type set to \"blog\"", layouts.LayoutDescriptor{Kind: "section", Type: "blog", Section: "posts", OutputFormatName: "html", Suffix: "html"}},
{"Section list for \"posts\" section with layout set to \"demoLayout\"", layouts.LayoutDescriptor{Kind: "section", Layout: demoLayout, Section: "posts", OutputFormatName: "html", Suffix: "html"}},
{"Taxonomy list in categories", LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category"}, HTMLFormat},
{"Taxonomy term in categories", LayoutDescriptor{Kind: "term", Type: "categories", Section: "category"}, HTMLFormat},
{"Taxonomy list in categories", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}},
{"Taxonomy term in categories", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}},
} {
l := NewLayoutHandler()
layouts, _ := l.For(example.d, example.f)
l := layouts.NewLayoutHandler()
layouts, _ := l.For(example.d)
basicExamples = append(basicExamples, Example{
Example: example.name,
Kind: example.d.Kind,
OutputFormat: example.f.Name,
Suffix: example.f.MediaType.FirstSuffix.Suffix,
OutputFormat: example.d.OutputFormatName,
Suffix: example.d.Suffix,
Layouts: makeLayoutsPresentable(layouts),
})
}

View File

@@ -1,4 +1,4 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,13 +11,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package output
package layouts
import (
"strings"
"sync"
"github.com/gohugoio/hugo/helpers"
)
// These may be used as content sections with potential conflicts. Avoid that.
@@ -43,6 +41,10 @@ type LayoutDescriptor struct {
// LayoutOverride indicates what we should only look for the above layout.
LayoutOverride bool
// From OutputFormat and MediaType.
OutputFormatName string
Suffix string
RenderingHook bool
Baseof bool
}
@@ -54,37 +56,31 @@ func (d LayoutDescriptor) isList() bool {
// LayoutHandler calculates the layout template to use to render a given output type.
type LayoutHandler struct {
mu sync.RWMutex
cache map[layoutCacheKey][]string
}
type layoutCacheKey struct {
d LayoutDescriptor
f string
cache map[LayoutDescriptor][]string
}
// NewLayoutHandler creates a new LayoutHandler.
func NewLayoutHandler() *LayoutHandler {
return &LayoutHandler{cache: make(map[layoutCacheKey][]string)}
return &LayoutHandler{cache: make(map[LayoutDescriptor][]string)}
}
// For returns a layout for the given LayoutDescriptor and options.
// Layouts are rendered and cached internally.
func (l *LayoutHandler) For(d LayoutDescriptor, f Format) ([]string, error) {
func (l *LayoutHandler) For(d LayoutDescriptor) ([]string, error) {
// We will get lots of requests for the same layouts, so avoid recalculations.
key := layoutCacheKey{d, f.Name}
l.mu.RLock()
if cacheVal, found := l.cache[key]; found {
if cacheVal, found := l.cache[d]; found {
l.mu.RUnlock()
return cacheVal, nil
}
l.mu.RUnlock()
layouts := resolvePageTemplate(d, f)
layouts := resolvePageTemplate(d)
layouts = helpers.UniqueStringsReuse(layouts)
layouts = uniqueStringsReuse(layouts)
l.mu.Lock()
l.cache[key] = layouts
l.cache[d] = layouts
l.mu.Unlock()
return layouts, nil
@@ -94,7 +90,7 @@ type layoutBuilder struct {
layoutVariations []string
typeVariations []string
d LayoutDescriptor
f Format
//f Format
}
func (l *layoutBuilder) addLayoutVariations(vars ...string) {
@@ -134,8 +130,8 @@ func (l *layoutBuilder) addKind() {
const renderingHookRoot = "/_markup"
func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
b := &layoutBuilder{d: d, f: f}
func resolvePageTemplate(d LayoutDescriptor) []string {
b := &layoutBuilder{d: d}
if !d.RenderingHook && d.Layout != "" {
b.addLayoutVariations(d.Layout)
@@ -190,7 +186,7 @@ func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
b.addTypeVariations("")
}
isRSS := f.Name == RSSFormat.Name
isRSS := strings.EqualFold(d.OutputFormatName, "rss")
if !d.RenderingHook && !d.Baseof && isRSS {
// The historic and common rss.xml case
b.addLayoutVariations("")
@@ -223,7 +219,7 @@ func (l *layoutBuilder) resolveVariations() []string {
var layouts []string
var variations []string
name := strings.ToLower(l.f.Name)
name := strings.ToLower(l.d.OutputFormatName)
if l.d.Lang != "" {
// We prefer the most specific type before language.
@@ -241,7 +237,7 @@ func (l *layoutBuilder) resolveVariations() []string {
continue
}
s := constructLayoutPath(typeVar, layoutVar, variation, l.f.MediaType.FirstSuffix.Suffix)
s := constructLayoutPath(typeVar, layoutVar, variation, l.d.Suffix)
if s != "" {
layouts = append(layouts, s)
}
@@ -300,3 +296,23 @@ func constructLayoutPath(typ, layout, variations, extension string) string {
return p.String()
}
// Inline this here so we can use tinygo to compile a wasm binary of this package.
func uniqueStringsReuse(s []string) []string {
result := s[:0]
for i, val := range s {
var seen bool
for j := 0; j < i; j++ {
if s[j] == val {
seen = true
break
}
}
if !seen {
result = append(result, val)
}
}
return result
}

View File

@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package output
package layouts
import (
"fmt"
@@ -19,8 +19,6 @@ import (
"strings"
"testing"
"github.com/gohugoio/hugo/media"
qt "github.com/frankban/quicktest"
"github.com/kylelemons/godebug/diff"
)
@@ -28,42 +26,16 @@ import (
func TestLayout(t *testing.T) {
c := qt.New(t)
noExtNoDelimMediaType := media.WithDelimiterAndSuffixes(media.TextType, "", "")
noExtMediaType := media.WithDelimiterAndSuffixes(media.TextType, ".", "")
var (
ampType = Format{
Name: "AMP",
MediaType: media.HTMLType,
BaseName: "index",
}
htmlFormat = HTMLFormat
noExtDelimFormat = Format{
Name: "NEM",
MediaType: noExtNoDelimMediaType,
BaseName: "_redirects",
}
noExt = Format{
Name: "NEX",
MediaType: noExtMediaType,
BaseName: "next",
}
)
for _, this := range []struct {
name string
layoutDescriptor LayoutDescriptor
layoutOverride string
format Format
expect []string
}{
{
"Home",
LayoutDescriptor{Kind: "home"},
"", ampType,
LayoutDescriptor{Kind: "home", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"index.amp.html",
"home.amp.html",
@@ -81,8 +53,8 @@ func TestLayout(t *testing.T) {
},
{
"Home baseof",
LayoutDescriptor{Kind: "home", Baseof: true},
"", ampType,
LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"index-baseof.amp.html",
"home-baseof.amp.html",
@@ -104,8 +76,8 @@ func TestLayout(t *testing.T) {
},
{
"Home, HTML",
LayoutDescriptor{Kind: "home"},
"", htmlFormat,
LayoutDescriptor{Kind: "home", OutputFormatName: "html", Suffix: "html"},
"",
// We will eventually get to index.html. This looks stuttery, but makes the lookup logic easy to understand.
[]string{
"index.html.html",
@@ -124,8 +96,8 @@ func TestLayout(t *testing.T) {
},
{
"Home, HTML, baseof",
LayoutDescriptor{Kind: "home", Baseof: true},
"", htmlFormat,
LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "html", Suffix: "html"},
"",
[]string{
"index-baseof.html.html",
"home-baseof.html.html",
@@ -147,8 +119,8 @@ func TestLayout(t *testing.T) {
},
{
"Home, french language",
LayoutDescriptor{Kind: "home", Lang: "fr"},
"", ampType,
LayoutDescriptor{Kind: "home", Lang: "fr", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"index.fr.amp.html",
"home.fr.amp.html",
@@ -178,8 +150,8 @@ func TestLayout(t *testing.T) {
},
{
"Home, no ext or delim",
LayoutDescriptor{Kind: "home"},
"", noExtDelimFormat,
LayoutDescriptor{Kind: "home", OutputFormatName: "nem", Suffix: ""},
"",
[]string{
"index.nem",
"home.nem",
@@ -191,8 +163,8 @@ func TestLayout(t *testing.T) {
},
{
"Home, no ext",
LayoutDescriptor{Kind: "home"},
"", noExt,
LayoutDescriptor{Kind: "home", OutputFormatName: "nex", Suffix: ""},
"",
[]string{
"index.nex",
"home.nex",
@@ -204,14 +176,14 @@ func TestLayout(t *testing.T) {
},
{
"Page, no ext or delim",
LayoutDescriptor{Kind: "page"},
"", noExtDelimFormat,
LayoutDescriptor{Kind: "page", OutputFormatName: "nem", Suffix: ""},
"",
[]string{"_default/single.nem"},
},
{
"Section",
LayoutDescriptor{Kind: "section", Section: "sect1"},
"", ampType,
LayoutDescriptor{Kind: "section", Section: "sect1", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"sect1/sect1.amp.html",
"sect1/section.amp.html",
@@ -235,8 +207,8 @@ func TestLayout(t *testing.T) {
},
{
"Section, baseof",
LayoutDescriptor{Kind: "section", Section: "sect1", Baseof: true},
"", ampType,
LayoutDescriptor{Kind: "section", Section: "sect1", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"sect1/sect1-baseof.amp.html",
"sect1/section-baseof.amp.html",
@@ -266,8 +238,8 @@ func TestLayout(t *testing.T) {
},
{
"Section, baseof, French, AMP",
LayoutDescriptor{Kind: "section", Section: "sect1", Lang: "fr", Baseof: true},
"", ampType,
LayoutDescriptor{Kind: "section", Section: "sect1", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"sect1/sect1-baseof.fr.amp.html",
"sect1/section-baseof.fr.amp.html",
@@ -321,8 +293,8 @@ func TestLayout(t *testing.T) {
},
{
"Section with layout",
LayoutDescriptor{Kind: "section", Section: "sect1", Layout: "mylayout"},
"", ampType,
LayoutDescriptor{Kind: "section", Section: "sect1", Layout: "mylayout", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"sect1/mylayout.amp.html",
"sect1/sect1.amp.html",
@@ -352,8 +324,8 @@ func TestLayout(t *testing.T) {
},
{
"Term, French, AMP",
LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr"},
"", ampType,
LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"term/term.fr.amp.html",
"term/tags.fr.amp.html",
@@ -423,8 +395,8 @@ func TestLayout(t *testing.T) {
},
{
"Term, baseof, French, AMP",
LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", Baseof: true},
"", ampType,
LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"term/term-baseof.fr.amp.html",
"term/tags-baseof.fr.amp.html",
@@ -510,8 +482,8 @@ func TestLayout(t *testing.T) {
},
{
"Term",
LayoutDescriptor{Kind: "term", Section: "tags"},
"", ampType,
LayoutDescriptor{Kind: "term", Section: "tags", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"term/term.amp.html",
"term/tags.amp.html",
@@ -549,8 +521,8 @@ func TestLayout(t *testing.T) {
},
{
"Taxonomy",
LayoutDescriptor{Kind: "taxonomy", Section: "categories"},
"", ampType,
LayoutDescriptor{Kind: "taxonomy", Section: "categories", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"categories/categories.terms.amp.html",
"categories/terms.amp.html",
@@ -580,8 +552,8 @@ func TestLayout(t *testing.T) {
},
{
"Page",
LayoutDescriptor{Kind: "page"},
"", ampType,
LayoutDescriptor{Kind: "page", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"_default/single.amp.html",
"_default/single.html",
@@ -589,8 +561,8 @@ func TestLayout(t *testing.T) {
},
{
"Page, baseof",
LayoutDescriptor{Kind: "page", Baseof: true},
"", ampType,
LayoutDescriptor{Kind: "page", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"_default/single-baseof.amp.html",
"_default/baseof.amp.html",
@@ -600,8 +572,8 @@ func TestLayout(t *testing.T) {
},
{
"Page with layout",
LayoutDescriptor{Kind: "page", Layout: "mylayout"},
"", ampType,
LayoutDescriptor{Kind: "page", Layout: "mylayout", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"_default/mylayout.amp.html",
"_default/single.amp.html",
@@ -611,8 +583,8 @@ func TestLayout(t *testing.T) {
},
{
"Page with layout, baseof",
LayoutDescriptor{Kind: "page", Layout: "mylayout", Baseof: true},
"", ampType,
LayoutDescriptor{Kind: "page", Layout: "mylayout", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"_default/mylayout-baseof.amp.html",
"_default/single-baseof.amp.html",
@@ -624,8 +596,8 @@ func TestLayout(t *testing.T) {
},
{
"Page with layout and type",
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype"},
"", ampType,
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"myttype/mylayout.amp.html",
"myttype/single.amp.html",
@@ -639,8 +611,8 @@ func TestLayout(t *testing.T) {
},
{
"Page baseof with layout and type",
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Baseof: true},
"", ampType,
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"myttype/mylayout-baseof.amp.html",
"myttype/single-baseof.amp.html",
@@ -658,8 +630,8 @@ func TestLayout(t *testing.T) {
},
{
"Page baseof with layout and type in French",
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Lang: "fr", Baseof: true},
"", ampType,
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"myttype/mylayout-baseof.fr.amp.html",
"myttype/single-baseof.fr.amp.html",
@@ -689,8 +661,8 @@ func TestLayout(t *testing.T) {
},
{
"Page with layout and type with subtype",
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype/mysubtype"},
"", ampType,
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype/mysubtype", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"myttype/mysubtype/mylayout.amp.html",
"myttype/mysubtype/single.amp.html",
@@ -705,8 +677,8 @@ func TestLayout(t *testing.T) {
// RSS
{
"RSS Home",
LayoutDescriptor{Kind: "home"},
"", RSSFormat,
LayoutDescriptor{Kind: "home", OutputFormatName: "rss", Suffix: "xml"},
"",
[]string{
"index.rss.xml",
"home.rss.xml",
@@ -727,8 +699,8 @@ func TestLayout(t *testing.T) {
},
{
"RSS Home, baseof",
LayoutDescriptor{Kind: "home", Baseof: true},
"", RSSFormat,
LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "rss", Suffix: "xml"},
"",
[]string{
"index-baseof.rss.xml",
"home-baseof.rss.xml",
@@ -750,8 +722,8 @@ func TestLayout(t *testing.T) {
},
{
"RSS Section",
LayoutDescriptor{Kind: "section", Section: "sect1"},
"", RSSFormat,
LayoutDescriptor{Kind: "section", Section: "sect1", OutputFormatName: "rss", Suffix: "xml"},
"",
[]string{
"sect1/sect1.rss.xml",
"sect1/section.rss.xml",
@@ -779,8 +751,8 @@ func TestLayout(t *testing.T) {
},
{
"RSS Term",
LayoutDescriptor{Kind: "term", Section: "tag"},
"", RSSFormat,
LayoutDescriptor{Kind: "term", Section: "tag", OutputFormatName: "rss", Suffix: "xml"},
"",
[]string{
"term/term.rss.xml",
"term/tag.rss.xml",
@@ -823,8 +795,8 @@ func TestLayout(t *testing.T) {
},
{
"RSS Taxonomy",
LayoutDescriptor{Kind: "taxonomy", Section: "tag"},
"", RSSFormat,
LayoutDescriptor{Kind: "taxonomy", Section: "tag", OutputFormatName: "rss", Suffix: "xml"},
"",
[]string{
"tag/tag.terms.rss.xml",
"tag/terms.rss.xml",
@@ -858,8 +830,8 @@ func TestLayout(t *testing.T) {
},
{
"Home plain text",
LayoutDescriptor{Kind: "home"},
"", JSONFormat,
LayoutDescriptor{Kind: "home", OutputFormatName: "json", Suffix: "json"},
"",
[]string{
"index.json.json",
"home.json.json",
@@ -877,8 +849,8 @@ func TestLayout(t *testing.T) {
},
{
"Page plain text",
LayoutDescriptor{Kind: "page"},
"", JSONFormat,
LayoutDescriptor{Kind: "page", OutputFormatName: "json", Suffix: "json"},
"",
[]string{
"_default/single.json.json",
"_default/single.json",
@@ -886,8 +858,8 @@ func TestLayout(t *testing.T) {
},
{
"Reserved section, shortcodes",
LayoutDescriptor{Kind: "section", Section: "shortcodes", Type: "shortcodes"},
"", ampType,
LayoutDescriptor{Kind: "section", Section: "shortcodes", Type: "shortcodes", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"section/shortcodes.amp.html",
"section/section.amp.html",
@@ -905,8 +877,8 @@ func TestLayout(t *testing.T) {
},
{
"Reserved section, partials",
LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials"},
"", ampType,
LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"section/partials.amp.html",
"section/section.amp.html",
@@ -925,8 +897,8 @@ func TestLayout(t *testing.T) {
// This is currently always HTML only
{
"404, HTML",
LayoutDescriptor{Kind: "404"},
"", htmlFormat,
LayoutDescriptor{Kind: "404", OutputFormatName: "html", Suffix: "html"},
"",
[]string{
"404.html.html",
"404.html",
@@ -934,8 +906,8 @@ func TestLayout(t *testing.T) {
},
{
"404, HTML baseof",
LayoutDescriptor{Kind: "404", Baseof: true},
"", htmlFormat,
LayoutDescriptor{Kind: "404", Baseof: true, OutputFormatName: "html", Suffix: "html"},
"",
[]string{
"404-baseof.html.html",
"baseof.html.html",
@@ -949,8 +921,8 @@ func TestLayout(t *testing.T) {
},
{
"Content hook",
LayoutDescriptor{Kind: "render-link", RenderingHook: true, Layout: "mylayout", Section: "blog"},
"", ampType,
LayoutDescriptor{Kind: "render-link", RenderingHook: true, Layout: "mylayout", Section: "blog", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"blog/_markup/render-link.amp.html",
"blog/_markup/render-link.html",
@@ -962,7 +934,7 @@ func TestLayout(t *testing.T) {
c.Run(this.name, func(c *qt.C) {
l := NewLayoutHandler()
layouts, err := l.For(this.layoutDescriptor, this.format)
layouts, err := l.For(this.layoutDescriptor)
c.Assert(err, qt.IsNil)
c.Assert(layouts, qt.Not(qt.IsNil), qt.Commentf(this.layoutDescriptor.Kind))
@@ -981,8 +953,10 @@ func TestLayout(t *testing.T) {
}
})
}
}
/*
func BenchmarkLayout(b *testing.B) {
descriptor := LayoutDescriptor{Kind: "taxonomy", Section: "categories"}
l := NewLayoutHandler()
@@ -1006,3 +980,4 @@ func BenchmarkLayoutUncached(b *testing.B) {
}
}
}
*/

View File

@@ -17,19 +17,18 @@ package output
import (
"encoding/json"
"fmt"
"reflect"
"sort"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/gohugoio/hugo/media"
)
// Format represents an output representation, usually to a file on disk.
// <docsmeta>{ "name": "OutputFormat" }</docsmeta>
type Format struct {
// The Name is used as an identifier. Internal output formats (i.e. HTML and RSS)
// The Name is used as an identifier. Internal output formats (i.e. html and rss)
// can be overridden by providing a new definition for those types.
// <docsmeta>{ "identifiers": ["html", "rss"] }</docsmeta>
Name string `json:"name"`
MediaType media.Type `json:"-"`
@@ -40,14 +39,7 @@ type Format struct {
// The base output file name used when not using "ugly URLs", defaults to "index".
BaseName string `json:"baseName"`
// The value to use for rel links
//
// See https://www.w3schools.com/tags/att_link_rel.asp
//
// AMP has a special requirement in this department, see:
// https://www.ampproject.org/docs/guides/deploy/discovery
// I.e.:
// <link rel="amphtml" href="https://www.example.com/url/to/amp/document.html">
// The value to use for rel links.
Rel string `json:"rel"`
// The protocol to use, i.e. "webcal://". Defaults to the protocol of the baseURL.
@@ -86,8 +78,8 @@ type Format struct {
// An ordered list of built-in output formats.
var (
AMPFormat = Format{
Name: "AMP",
MediaType: media.HTMLType,
Name: "amp",
MediaType: media.Builtin.HTMLType,
BaseName: "index",
Path: "amp",
Rel: "amphtml",
@@ -97,8 +89,8 @@ var (
}
CalendarFormat = Format{
Name: "Calendar",
MediaType: media.CalendarType,
Name: "calendar",
MediaType: media.Builtin.CalendarType,
IsPlainText: true,
Protocol: "webcal://",
BaseName: "index",
@@ -106,24 +98,24 @@ var (
}
CSSFormat = Format{
Name: "CSS",
MediaType: media.CSSType,
Name: "css",
MediaType: media.Builtin.CSSType,
BaseName: "styles",
IsPlainText: true,
Rel: "stylesheet",
NotAlternative: true,
}
CSVFormat = Format{
Name: "CSV",
MediaType: media.CSVType,
Name: "csv",
MediaType: media.Builtin.CSVType,
BaseName: "index",
IsPlainText: true,
Rel: "alternate",
}
HTMLFormat = Format{
Name: "HTML",
MediaType: media.HTMLType,
Name: "html",
MediaType: media.Builtin.HTMLType,
BaseName: "index",
Rel: "canonical",
IsHTML: true,
@@ -135,24 +127,24 @@ var (
}
MarkdownFormat = Format{
Name: "MARKDOWN",
MediaType: media.MarkdownType,
Name: "markdown",
MediaType: media.Builtin.MarkdownType,
BaseName: "index",
Rel: "alternate",
IsPlainText: true,
}
JSONFormat = Format{
Name: "JSON",
MediaType: media.JSONType,
Name: "json",
MediaType: media.Builtin.JSONType,
BaseName: "index",
IsPlainText: true,
Rel: "alternate",
}
WebAppManifestFormat = Format{
Name: "WebAppManifest",
MediaType: media.WebAppManifestType,
Name: "webappmanifest",
MediaType: media.Builtin.WebAppManifestType,
BaseName: "manifest",
IsPlainText: true,
NotAlternative: true,
@@ -160,24 +152,24 @@ var (
}
RobotsTxtFormat = Format{
Name: "ROBOTS",
MediaType: media.TextType,
Name: "robots",
MediaType: media.Builtin.TextType,
BaseName: "robots",
IsPlainText: true,
Rel: "alternate",
}
RSSFormat = Format{
Name: "RSS",
MediaType: media.RSSType,
Name: "rss",
MediaType: media.Builtin.RSSType,
BaseName: "index",
NoUgly: true,
Rel: "alternate",
}
SitemapFormat = Format{
Name: "Sitemap",
MediaType: media.XMLType,
Name: "sitemap",
MediaType: media.Builtin.XMLType,
BaseName: "sitemap",
NoUgly: true,
Rel: "sitemap",
@@ -204,6 +196,7 @@ func init() {
}
// Formats is a slice of Format.
// <docsmeta>{ "name": "OutputFormats" }</docsmeta>
type Formats []Format
func (formats Formats) Len() int { return len(formats) }
@@ -298,102 +291,6 @@ func (formats Formats) FromFilename(filename string) (f Format, found bool) {
return
}
// DecodeFormats takes a list of output format configurations and merges those,
// in the order given, with the Hugo defaults as the last resort.
func DecodeFormats(mediaTypes media.Types, maps ...map[string]any) (Formats, error) {
f := make(Formats, len(DefaultFormats))
copy(f, DefaultFormats)
for _, m := range maps {
for k, v := range m {
found := false
for i, vv := range f {
if strings.EqualFold(k, vv.Name) {
// Merge it with the existing
if err := decode(mediaTypes, v, &f[i]); err != nil {
return f, err
}
found = true
}
}
if !found {
var newOutFormat Format
newOutFormat.Name = k
if err := decode(mediaTypes, v, &newOutFormat); err != nil {
return f, err
}
// We need values for these
if newOutFormat.BaseName == "" {
newOutFormat.BaseName = "index"
}
if newOutFormat.Rel == "" {
newOutFormat.Rel = "alternate"
}
f = append(f, newOutFormat)
}
}
}
sort.Sort(f)
return f, nil
}
func decode(mediaTypes media.Types, input any, output *Format) error {
config := &mapstructure.DecoderConfig{
Metadata: nil,
Result: output,
WeaklyTypedInput: true,
DecodeHook: func(a reflect.Type, b reflect.Type, c any) (any, error) {
if a.Kind() == reflect.Map {
dataVal := reflect.Indirect(reflect.ValueOf(c))
for _, key := range dataVal.MapKeys() {
keyStr, ok := key.Interface().(string)
if !ok {
// Not a string key
continue
}
if strings.EqualFold(keyStr, "mediaType") {
// If mediaType is a string, look it up and replace it
// in the map.
vv := dataVal.MapIndex(key)
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", vviv)
}
dataVal.SetMapIndex(key, reflect.ValueOf(mediaType))
default:
return nil, fmt.Errorf("invalid output format configuration; wrong type for media type, expected string (e.g. text/html), got %T", vvi)
}
}
}
}
return c, nil
},
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
return err
}
if err = decoder.Decode(input); err != nil {
return fmt.Errorf("failed to decode output format configuration: %w", err)
}
return nil
}
// BaseFilename returns the base filename of f including an extension (ie.
// "index.xml").
func (f Format) BaseFilename() string {

View File

@@ -23,46 +23,46 @@ import (
func TestDefaultTypes(t *testing.T) {
c := qt.New(t)
c.Assert(CalendarFormat.Name, qt.Equals, "Calendar")
c.Assert(CalendarFormat.MediaType, qt.Equals, media.CalendarType)
c.Assert(CalendarFormat.Name, qt.Equals, "calendar")
c.Assert(CalendarFormat.MediaType, qt.Equals, media.Builtin.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, qt.Equals, media.CSSType)
c.Assert(CSSFormat.Name, qt.Equals, "css")
c.Assert(CSSFormat.MediaType, qt.Equals, media.Builtin.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, qt.Equals, media.CSVType)
c.Assert(CSVFormat.Name, qt.Equals, "csv")
c.Assert(CSVFormat.MediaType, qt.Equals, media.Builtin.CSVType)
c.Assert(CSVFormat.Path, qt.HasLen, 0)
c.Assert(CSVFormat.Protocol, qt.HasLen, 0)
c.Assert(CSVFormat.IsPlainText, qt.Equals, true)
c.Assert(CSVFormat.IsHTML, qt.Equals, false)
c.Assert(CSVFormat.Permalinkable, qt.Equals, false)
c.Assert(HTMLFormat.Name, qt.Equals, "HTML")
c.Assert(HTMLFormat.MediaType, qt.Equals, media.HTMLType)
c.Assert(HTMLFormat.Name, qt.Equals, "html")
c.Assert(HTMLFormat.MediaType, qt.Equals, media.Builtin.HTMLType)
c.Assert(HTMLFormat.Path, qt.HasLen, 0)
c.Assert(HTMLFormat.Protocol, qt.HasLen, 0)
c.Assert(HTMLFormat.IsPlainText, qt.Equals, false)
c.Assert(HTMLFormat.IsHTML, qt.Equals, true)
c.Assert(AMPFormat.Permalinkable, qt.Equals, true)
c.Assert(AMPFormat.Name, qt.Equals, "AMP")
c.Assert(AMPFormat.MediaType, qt.Equals, media.HTMLType)
c.Assert(AMPFormat.Name, qt.Equals, "amp")
c.Assert(AMPFormat.MediaType, qt.Equals, media.Builtin.HTMLType)
c.Assert(AMPFormat.Path, qt.Equals, "amp")
c.Assert(AMPFormat.Protocol, qt.HasLen, 0)
c.Assert(AMPFormat.IsPlainText, qt.Equals, false)
c.Assert(AMPFormat.IsHTML, qt.Equals, true)
c.Assert(AMPFormat.Permalinkable, qt.Equals, true)
c.Assert(RSSFormat.Name, qt.Equals, "RSS")
c.Assert(RSSFormat.MediaType, qt.Equals, media.RSSType)
c.Assert(RSSFormat.Name, qt.Equals, "rss")
c.Assert(RSSFormat.MediaType, qt.Equals, media.Builtin.RSSType)
c.Assert(RSSFormat.Path, qt.HasLen, 0)
c.Assert(RSSFormat.IsPlainText, qt.Equals, false)
c.Assert(RSSFormat.NoUgly, qt.Equals, true)
@@ -101,10 +101,10 @@ func TestGetFormatByExt(t *testing.T) {
func TestGetFormatByFilename(t *testing.T) {
c := qt.New(t)
noExtNoDelimMediaType := media.TextType
noExtNoDelimMediaType := media.Builtin.TextType
noExtNoDelimMediaType.Delimiter = ""
noExtMediaType := media.TextType
noExtMediaType := media.Builtin.TextType
var (
noExtDelimFormat = Format{
@@ -138,117 +138,10 @@ func TestGetFormatByFilename(t *testing.T) {
c.Assert(found, qt.Equals, false)
}
func TestDecodeFormats(t *testing.T) {
c := qt.New(t)
mediaTypes := media.Types{media.JSONType, media.XMLType}
tests := []struct {
name string
maps []map[string]any
shouldError bool
assert func(t *testing.T, name string, f Formats)
}{
{
"Redefine JSON",
[]map[string]any{
{
"JsON": map[string]any{
"baseName": "myindex",
"isPlainText": "false",
},
},
},
false,
func(t *testing.T, name string, f Formats) {
msg := qt.Commentf(name)
c.Assert(len(f), qt.Equals, len(DefaultFormats), msg)
json, _ := f.GetByName("JSON")
c.Assert(json.BaseName, qt.Equals, "myindex")
c.Assert(json.MediaType, qt.Equals, media.JSONType)
c.Assert(json.IsPlainText, qt.Equals, false)
},
},
{
"Add XML format with string as mediatype",
[]map[string]any{
{
"MYXMLFORMAT": map[string]any{
"baseName": "myxml",
"mediaType": "application/xml",
},
},
},
false,
func(t *testing.T, name string, f Formats) {
c.Assert(len(f), qt.Equals, len(DefaultFormats)+1)
xml, found := f.GetByName("MYXMLFORMAT")
c.Assert(found, qt.Equals, true)
c.Assert(xml.BaseName, qt.Equals, "myxml")
c.Assert(xml.MediaType, qt.Equals, media.XMLType)
// Verify that we haven't changed the DefaultFormats slice.
json, _ := f.GetByName("JSON")
c.Assert(json.BaseName, qt.Equals, "index")
},
},
{
"Add format unknown mediatype",
[]map[string]any{
{
"MYINVALID": map[string]any{
"baseName": "mymy",
"mediaType": "application/hugo",
},
},
},
true,
func(t *testing.T, name string, f Formats) {
},
},
{
"Add and redefine XML format",
[]map[string]any{
{
"MYOTHERXMLFORMAT": map[string]any{
"baseName": "myotherxml",
"mediaType": media.XMLType,
},
},
{
"MYOTHERXMLFORMAT": map[string]any{
"baseName": "myredefined",
},
},
},
false,
func(t *testing.T, name string, f Formats) {
c.Assert(len(f), qt.Equals, len(DefaultFormats)+1)
xml, found := f.GetByName("MYOTHERXMLFORMAT")
c.Assert(found, qt.Equals, true)
c.Assert(xml.BaseName, qt.Equals, "myredefined")
c.Assert(xml.MediaType, qt.Equals, media.XMLType)
},
},
}
for _, test := range tests {
result, err := DecodeFormats(mediaTypes, test.maps...)
msg := qt.Commentf(test.name)
if test.shouldError {
c.Assert(err, qt.Not(qt.IsNil), msg)
} else {
c.Assert(err, qt.IsNil, msg)
test.assert(t, test.name, result)
}
}
}
func TestSort(t *testing.T) {
c := qt.New(t)
c.Assert(DefaultFormats[0].Name, qt.Equals, "HTML")
c.Assert(DefaultFormats[1].Name, qt.Equals, "AMP")
c.Assert(DefaultFormats[0].Name, qt.Equals, "html")
c.Assert(DefaultFormats[1].Name, qt.Equals, "amp")
json := JSONFormat
json.Weight = 1
@@ -261,7 +154,7 @@ func TestSort(t *testing.T) {
sort.Sort(formats)
c.Assert(formats[0].Name, qt.Equals, "JSON")
c.Assert(formats[1].Name, qt.Equals, "HTML")
c.Assert(formats[2].Name, qt.Equals, "AMP")
c.Assert(formats[0].Name, qt.Equals, "json")
c.Assert(formats[1].Name, qt.Equals, "html")
c.Assert(formats[2].Name, qt.Equals, "amp")
}