shithub: hugo

Download patch

ref: c9aee467d387c4c3489c23f120a7ef2fed4d12df
parent: d6e8b86f66d6d505fadc32bca601762a4aa90c5e
author: Bjørn Erik Pedersen <[email protected]>
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)
+		}
+	}
 }