mirror of
https://github.com/gohugoio/hugo.git
synced 2025-03-14 07:55:37 +00:00
output: Add output formats decoder
And clean up the output package.
This commit is contained in:
parent
d6e8b86f66
commit
c9aee467d3
6 changed files with 281 additions and 91 deletions
|
@ -909,7 +909,7 @@ func (p *Page) update(f interface{}) error {
|
||||||
o := cast.ToStringSlice(v)
|
o := cast.ToStringSlice(v)
|
||||||
if len(o) > 0 {
|
if len(o) > 0 {
|
||||||
// Output formats are exlicitly set in front matter, use those.
|
// Output formats are exlicitly set in front matter, use those.
|
||||||
outFormats, err := output.GetFormats(o...)
|
outFormats, err := output.DefaultFormats.GetByNames(o...)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.s.Log.ERROR.Printf("Failed to resolve output formats: %s", err)
|
p.s.Log.ERROR.Printf("Failed to resolve output formats: %s", err)
|
||||||
|
|
|
@ -40,7 +40,7 @@ func createSiteOutputFormats(cfg config.Provider) (map[string]output.Formats, er
|
||||||
var formats output.Formats
|
var formats output.Formats
|
||||||
vals := cast.ToStringSlice(v)
|
vals := cast.ToStringSlice(v)
|
||||||
for _, format := range vals {
|
for _, format := range vals {
|
||||||
f, found := output.GetFormat(format)
|
f, found := output.DefaultFormats.GetByName(format)
|
||||||
if !found {
|
if !found {
|
||||||
return nil, fmt.Errorf("Failed to resolve output format %q from site config", format)
|
return nil, fmt.Errorf("Failed to resolve output format %q from site config", format)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,10 +15,20 @@ package media
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Types []Type
|
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
|
// 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.
|
// file formats and format contents transmitted on the Internet.
|
||||||
// For Hugo's use case, we use the top-level type name / subtype name + suffix.
|
// For Hugo's use case, we use the top-level type name / subtype name + suffix.
|
||||||
|
|
|
@ -47,3 +47,14 @@ func TestDefaultTypes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -15,11 +15,55 @@ package output
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
|
||||||
"github.com/spf13/hugo/media"
|
"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 (
|
var (
|
||||||
// An ordered list of built-in output formats
|
// An ordered list of built-in output formats
|
||||||
//
|
//
|
||||||
|
@ -33,7 +77,6 @@ var (
|
||||||
IsHTML: true,
|
IsHTML: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// CalendarFormat is AAA
|
|
||||||
CalendarFormat = Format{
|
CalendarFormat = Format{
|
||||||
Name: "Calendar",
|
Name: "Calendar",
|
||||||
MediaType: media.CalendarType,
|
MediaType: media.CalendarType,
|
||||||
|
@ -83,32 +126,33 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var builtInTypes = map[string]Format{
|
var DefaultFormats = Formats{
|
||||||
strings.ToLower(AMPFormat.Name): AMPFormat,
|
AMPFormat,
|
||||||
strings.ToLower(CalendarFormat.Name): CalendarFormat,
|
CalendarFormat,
|
||||||
strings.ToLower(CSSFormat.Name): CSSFormat,
|
CSSFormat,
|
||||||
strings.ToLower(CSVFormat.Name): CSVFormat,
|
CSVFormat,
|
||||||
strings.ToLower(HTMLFormat.Name): HTMLFormat,
|
HTMLFormat,
|
||||||
strings.ToLower(JSONFormat.Name): JSONFormat,
|
JSONFormat,
|
||||||
strings.ToLower(RSSFormat.Name): RSSFormat,
|
RSSFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sort.Sort(DefaultFormats)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Formats []Format
|
type Formats []Format
|
||||||
|
|
||||||
func (formats Formats) GetByName(name string) (f Format, found bool) {
|
func (f Formats) Len() int { return len(f) }
|
||||||
for _, ff := range formats {
|
func (f Formats) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
|
||||||
if name == ff.Name {
|
func (f Formats) Less(i, j int) bool { return f[i].Name < f[j].Name }
|
||||||
f = ff
|
|
||||||
found = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (formats Formats) GetBySuffix(name string) (f Format, found bool) {
|
// 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 {
|
for _, ff := range formats {
|
||||||
if name == ff.MediaType.Suffix {
|
if strings.EqualFold(suffix, ff.MediaType.Suffix) {
|
||||||
if found {
|
if found {
|
||||||
// ambiguous
|
// ambiguous
|
||||||
found = false
|
found = false
|
||||||
|
@ -121,6 +165,33 @@ func (formats Formats) GetBySuffix(name string) (f Format, found bool) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetByName gets a format by its identifier name.
|
||||||
|
func (formats Formats) GetByName(name string) (f Format, found bool) {
|
||||||
|
for _, ff := range formats {
|
||||||
|
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) {
|
func (formats Formats) FromFilename(filename string) (f Format, found bool) {
|
||||||
// mytemplate.amp.html
|
// mytemplate.amp.html
|
||||||
// mytemplate.html
|
// mytemplate.html
|
||||||
|
@ -145,66 +216,79 @@ func (formats Formats) FromFilename(filename string) (f Format, found bool) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format represents an output representation, usually to a file on disk.
|
// DecodeOutputFormats takes a list of output format configurations and merges those,
|
||||||
type Format struct {
|
// in ther order given, with the Hugo defaults as the last resort.
|
||||||
// The Name is used as an identifier. Internal output formats (i.e. HTML and RSS)
|
func DecodeOutputFormats(mediaTypes media.Types, maps ...map[string]interface{}) (Formats, error) {
|
||||||
// can be overridden by providing a new definition for those types.
|
f := make(Formats, len(DefaultFormats))
|
||||||
Name string
|
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.
|
f = append(f, newOutFormat)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetFormat(key string) (Format, bool) {
|
|
||||||
found, ok := builtInTypes[key]
|
|
||||||
if !ok {
|
|
||||||
found, ok = builtInTypes[strings.ToLower(key)]
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return types, nil
|
sort.Sort(f)
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder, err := mapstructure.NewDecoder(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoder.Decode(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Format) BaseFilename() string {
|
func (t Format) BaseFilename() string {
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
package output
|
package output
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/spf13/hugo/media"
|
"github.com/spf13/hugo/media"
|
||||||
|
@ -65,18 +66,9 @@ func TestDefaultTypes(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetFormat(t *testing.T) {
|
func TestGetFormatByName(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) {
|
|
||||||
formats := Formats{AMPFormat, CalendarFormat}
|
formats := Formats{AMPFormat, CalendarFormat}
|
||||||
tp, _ := formats.GetByName("AMP")
|
tp, _ := formats.GetByName("AMp")
|
||||||
require.Equal(t, AMPFormat, tp)
|
require.Equal(t, AMPFormat, tp)
|
||||||
_, found := formats.GetByName("HTML")
|
_, found := formats.GetByName("HTML")
|
||||||
require.False(t, found)
|
require.False(t, found)
|
||||||
|
@ -84,7 +76,7 @@ func TestGeGetFormatByName(t *testing.T) {
|
||||||
require.False(t, found)
|
require.False(t, found)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGeGetFormatByExt(t *testing.T) {
|
func TestGetFormatByExt(t *testing.T) {
|
||||||
formats1 := Formats{AMPFormat, CalendarFormat}
|
formats1 := Formats{AMPFormat, CalendarFormat}
|
||||||
formats2 := Formats{AMPFormat, HTMLFormat, CalendarFormat}
|
formats2 := Formats{AMPFormat, HTMLFormat, CalendarFormat}
|
||||||
tp, _ := formats1.GetBySuffix("html")
|
tp, _ := formats1.GetBySuffix("html")
|
||||||
|
@ -95,6 +87,99 @@ func TestGeGetFormatByExt(t *testing.T) {
|
||||||
require.False(t, found)
|
require.False(t, found)
|
||||||
|
|
||||||
// ambiguous
|
// ambiguous
|
||||||
_, found = formats2.GetByName("html")
|
_, found = formats2.GetBySuffix("html")
|
||||||
require.False(t, found)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue