ref: c9aee467d387c4c3489c23f120a7ef2fed4d12df
parent: d6e8b86f66d6d505fadc32bca601762a4aa90c5e
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Mon Apr 3 13:00:23 EDT 2017
output: Add output formats decoder And clean up the output package.
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -909,7 +909,7 @@
o := cast.ToStringSlice(v)
if len(o) > 0 {// Output formats are exlicitly set in front matter, use those.
- outFormats, err := output.GetFormats(o...)
+ outFormats, err := output.DefaultFormats.GetByNames(o...)
if err != nil { p.s.Log.ERROR.Printf("Failed to resolve output formats: %s", err)--- a/hugolib/site_output.go
+++ b/hugolib/site_output.go
@@ -40,7 +40,7 @@
var formats output.Formats
vals := cast.ToStringSlice(v)
for _, format := range vals {- f, found := output.GetFormat(format)
+ f, found := output.DefaultFormats.GetByName(format)
if !found { return nil, fmt.Errorf("Failed to resolve output format %q from site config", format)}
--- a/media/mediaType.go
+++ b/media/mediaType.go
@@ -15,9 +15,19 @@
import (
"fmt"
+ "strings"
)
type Types []Type
+
+func (t Types) GetByType(tp string) (Type, bool) {+ for _, tt := range t {+ if strings.EqualFold(tt.Type(), tp) {+ return tt, true
+ }
+ }
+ return Type{}, false+}
// A media type (also known as MIME type and content type) is a two-part identifier for
// file formats and format contents transmitted on the Internet.
--- a/media/mediaType_test.go
+++ b/media/mediaType_test.go
@@ -47,3 +47,14 @@
}
}
+
+func TestGetByType(t *testing.T) {+ types := Types{HTMLType, RSSType}+
+ mt, found := types.GetByType("text/HTML")+ require.True(t, found)
+ require.Equal(t, mt, HTMLType)
+
+ _, found = types.GetByType("text/nono")+ require.False(t, found)
+}
--- a/output/outputFormat.go
+++ b/output/outputFormat.go
@@ -15,11 +15,55 @@
import (
"fmt"
+ "sort"
"strings"
+ "reflect"
+
+ "github.com/mitchellh/mapstructure"
+
"github.com/spf13/hugo/media"
)
+// Format represents an output representation, usually to a file on disk.
+type Format struct {+ // 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.
+ Name string
+
+ MediaType media.Type
+
+ // Must be set to a value when there are two or more conflicting mediatype for the same resource.
+ Path string
+
+ // The base output file name used when not using "ugly URLs", defaults to "index".
+ BaseName string
+
+ // 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">
+ Rel string
+
+ // The protocol to use, i.e. "webcal://". Defaults to the protocol of the baseURL.
+ Protocol string
+
+ // IsPlainText decides whether to use text/template or html/template
+ // as template parser.
+ IsPlainText bool
+
+ // IsHTML returns whether this format is int the HTML family. This includes
+ // HTML, AMP etc. This is used to decide when to create alias redirects etc.
+ IsHTML bool
+
+ // Enable to ignore the global uglyURLs setting.
+ NoUgly bool
+}
+
var (
// An ordered list of built-in output formats
//
@@ -33,7 +77,6 @@
IsHTML: true,
}
- // CalendarFormat is AAA
CalendarFormat = Format{Name: "Calendar",
MediaType: media.CalendarType,
@@ -83,44 +126,72 @@
}
)
-var builtInTypes = map[string]Format{- strings.ToLower(AMPFormat.Name): AMPFormat,
- strings.ToLower(CalendarFormat.Name): CalendarFormat,
- strings.ToLower(CSSFormat.Name): CSSFormat,
- strings.ToLower(CSVFormat.Name): CSVFormat,
- strings.ToLower(HTMLFormat.Name): HTMLFormat,
- strings.ToLower(JSONFormat.Name): JSONFormat,
- strings.ToLower(RSSFormat.Name): RSSFormat,
+var DefaultFormats = Formats{+ AMPFormat,
+ CalendarFormat,
+ CSSFormat,
+ CSVFormat,
+ HTMLFormat,
+ JSONFormat,
+ RSSFormat,
}
+func init() {+ sort.Sort(DefaultFormats)
+}
+
type Formats []Format
-func (formats Formats) GetByName(name string) (f Format, found bool) {+func (f Formats) Len() int { return len(f) }+func (f Formats) Swap(i, j int) { f[i], f[j] = f[j], f[i] }+func (f Formats) Less(i, j int) bool { return f[i].Name < f[j].Name }+
+// GetBySuffix gets a output format given as suffix, e.g. "html".
+// It will return false if no format could be found, or if the suffix given
+// is ambiguous.
+// The lookup is case insensitive.
+func (formats Formats) GetBySuffix(suffix string) (f Format, found bool) { for _, ff := range formats {- if name == ff.Name {+ if strings.EqualFold(suffix, ff.MediaType.Suffix) {+ if found {+ // ambiguous
+ found = false
+ return
+ }
f = ff
found = true
- return
}
}
return
}
-func (formats Formats) GetBySuffix(name string) (f Format, found bool) {+// GetByName gets a format by its identifier name.
+func (formats Formats) GetByName(name string) (f Format, found bool) { for _, ff := range formats {- if name == ff.MediaType.Suffix {- if found {- // ambiguous
- found = false
- return
- }
+ if strings.EqualFold(name, ff.Name) {f = ff
found = true
+ return
}
}
return
}
+// GetByNames gets a list of formats given a list of identifiers.
+func (formats Formats) GetByNames(names ...string) (Formats, error) {+ var types []Format
+
+ for _, name := range names {+ tpe, ok := formats.GetByName(name)
+ if !ok {+ return types, fmt.Errorf("OutputFormat with key %q not found", name)+ }
+ types = append(types, tpe)
+ }
+ return types, nil
+}
+
+// FromFilename gets a Format given a filename.
func (formats Formats) FromFilename(filename string) (f Format, found bool) {// mytemplate.amp.html
// mytemplate.html
@@ -145,66 +216,79 @@
return
}
-// Format represents an output representation, usually to a file on disk.
-type Format struct {- // 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.
- Name string
+// DecodeOutputFormats takes a list of output format configurations and merges those,
+// in ther order given, with the Hugo defaults as the last resort.
+func DecodeOutputFormats(mediaTypes media.Types, maps ...map[string]interface{}) (Formats, error) {+ f := make(Formats, len(DefaultFormats))
+ copy(f, DefaultFormats)
- MediaType media.Type
+ 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
+ }
- // Must be set to a value when there are two or more conflicting mediatype for the same resource.
- Path string
+ f = append(f, newOutFormat)
+ }
+ }
+ }
- // The base output file name used when not using "ugly URLs", defaults to "index".
- BaseName string
+ sort.Sort(f)
- // 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">
- Rel string
-
- // The protocol to use, i.e. "webcal://". Defaults to the protocol of the baseURL.
- Protocol string
-
- // IsPlainText decides whether to use text/template or html/template
- // as template parser.
- IsPlainText bool
-
- // IsHTML returns whether this format is int the HTML family. This includes
- // HTML, AMP etc. This is used to decide when to create alias redirects etc.
- IsHTML bool
-
- // Enable to ignore the global uglyURLs setting.
- NoUgly bool
+ return f, nil
}
-func GetFormat(key string) (Format, bool) {- found, ok := builtInTypes[key]
- if !ok {- found, ok = builtInTypes[strings.ToLower(key)]
+func decode(mediaTypes media.Types, input, output interface{}) error {+ config := &mapstructure.DecoderConfig{+ Metadata: nil,
+ Result: output,
+ WeaklyTypedInput: true,
+ DecodeHook: func(a reflect.Type, b reflect.Type, c interface{}) (interface{}, 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)
+ if mediaTypeStr, ok := vv.Interface().(string); ok {+ mediaType, found := mediaTypes.GetByType(mediaTypeStr)
+ if !found {+ return c, fmt.Errorf("media type %q not found", mediaTypeStr)+ }
+ dataVal.SetMapIndex(key, reflect.ValueOf(mediaType))
+ }
+ }
+ }
+ }
+ return c, nil
+ },
}
- return found, ok
-}
-// TODO(bep) outputs rewamp on global config?
-func GetFormats(keys ...string) (Formats, error) {- var types []Format
-
- for _, key := range keys {- tpe, ok := GetFormat(key)
- if !ok {- return types, fmt.Errorf("OutputFormat with key %q not found", key)- }
- types = append(types, tpe)
+ decoder, err := mapstructure.NewDecoder(config)
+ if err != nil {+ return err
}
- return types, nil
+ return decoder.Decode(input)
}
func (t Format) BaseFilename() string {--- a/output/outputFormat_test.go
+++ b/output/outputFormat_test.go
@@ -14,6 +14,7 @@
package output
import (
+ "fmt"
"testing"
"github.com/spf13/hugo/media"
@@ -65,18 +66,9 @@
}
-func TestGetFormat(t *testing.T) {- tp, _ := GetFormat("html")- require.Equal(t, HTMLFormat, tp)
- tp, _ = GetFormat("HTML")- require.Equal(t, HTMLFormat, tp)
- _, found := GetFormat("FOO")- require.False(t, found)
-}
-
-func TestGeGetFormatByName(t *testing.T) {+func TestGetFormatByName(t *testing.T) { formats := Formats{AMPFormat, CalendarFormat}- tp, _ := formats.GetByName("AMP")+ tp, _ := formats.GetByName("AMp")require.Equal(t, AMPFormat, tp)
_, found := formats.GetByName("HTML")require.False(t, found)
@@ -84,7 +76,7 @@
require.False(t, found)
}
-func TestGeGetFormatByExt(t *testing.T) {+func TestGetFormatByExt(t *testing.T) { formats1 := Formats{AMPFormat, CalendarFormat} formats2 := Formats{AMPFormat, HTMLFormat, CalendarFormat} tp, _ := formats1.GetBySuffix("html")@@ -95,6 +87,99 @@
require.False(t, found)
// ambiguous
- _, found = formats2.GetByName("html")+ _, found = formats2.GetBySuffix("html")require.False(t, found)
+}
+
+func TestDecodeFormats(t *testing.T) {+
+ mediaTypes := media.Types{media.JSONType, media.XMLType}+
+ var tests = []struct {+ name string
+ maps []map[string]interface{}+ shouldError bool
+ assert func(t *testing.T, name string, f Formats)
+ }{+ {+ "Redefine JSON",
+ []map[string]interface{}{+ map[string]interface{}{+ "JsON": map[string]interface{}{+ "baseName": "myindex",
+ "isPlainText": "false"}}},
+ false,
+ func(t *testing.T, name string, f Formats) {+ require.Len(t, f, len(DefaultFormats), name)
+ json, _ := f.GetByName("JSON")+ require.Equal(t, "myindex", json.BaseName)
+ require.Equal(t, media.JSONType, json.MediaType)
+ require.False(t, json.IsPlainText)
+
+ }},
+ {+ "Add XML format with string as mediatype",
+ []map[string]interface{}{+ map[string]interface{}{+ "MYXMLFORMAT": map[string]interface{}{+ "baseName": "myxml",
+ "mediaType": "application/xml",
+ }}},
+ false,
+ func(t *testing.T, name string, f Formats) {+ require.Len(t, f, len(DefaultFormats)+1, name)
+ xml, found := f.GetByName("MYXMLFORMAT")+ require.True(t, found)
+ require.Equal(t, "myxml", xml.BaseName, fmt.Sprint(xml))
+ require.Equal(t, media.XMLType, xml.MediaType)
+
+ // Verify that we haven't changed the DefaultFormats slice.
+ json, _ := f.GetByName("JSON")+ require.Equal(t, "index", json.BaseName, name)
+
+ }},
+ {+ "Add format unknown mediatype",
+ []map[string]interface{}{+ map[string]interface{}{+ "MYINVALID": map[string]interface{}{+ "baseName": "mymy",
+ "mediaType": "application/hugo",
+ }}},
+ true,
+ func(t *testing.T, name string, f Formats) {+
+ }},
+ {+ "Add and redefine XML format",
+ []map[string]interface{}{+ map[string]interface{}{+ "MYOTHERXMLFORMAT": map[string]interface{}{+ "baseName": "myotherxml",
+ "mediaType": media.XMLType,
+ }},
+ map[string]interface{}{+ "MYOTHERXMLFORMAT": map[string]interface{}{+ "baseName": "myredefined",
+ }},
+ },
+ false,
+ func(t *testing.T, name string, f Formats) {+ require.Len(t, f, len(DefaultFormats)+1, name)
+ xml, found := f.GetByName("MYOTHERXMLFORMAT")+ require.True(t, found)
+ require.Equal(t, "myredefined", xml.BaseName, fmt.Sprint(xml))
+ require.Equal(t, media.XMLType, xml.MediaType)
+ }},
+ }
+
+ for _, test := range tests {+ result, err := DecodeOutputFormats(mediaTypes, test.maps...)
+ if test.shouldError {+ require.Error(t, err, test.name)
+ } else {+ require.NoError(t, err, test.name)
+ test.assert(t, test.name, result)
+ }
+ }
}
--
⑨