github.com/gohugoio/hugo@v0.88.1/output/outputFormat.go (about)

     1  // Copyright 2019 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package output
    15  
    16  import (
    17  	"encoding/json"
    18  	"fmt"
    19  	"reflect"
    20  	"sort"
    21  	"strings"
    22  
    23  	"github.com/pkg/errors"
    24  
    25  	"github.com/mitchellh/mapstructure"
    26  
    27  	"github.com/gohugoio/hugo/media"
    28  )
    29  
    30  // Format represents an output representation, usually to a file on disk.
    31  type Format struct {
    32  	// The Name is used as an identifier. Internal output formats (i.e. HTML and RSS)
    33  	// can be overridden by providing a new definition for those types.
    34  	Name string `json:"name"`
    35  
    36  	MediaType media.Type `json:"-"`
    37  
    38  	// Must be set to a value when there are two or more conflicting mediatype for the same resource.
    39  	Path string `json:"path"`
    40  
    41  	// The base output file name used when not using "ugly URLs", defaults to "index".
    42  	BaseName string `json:"baseName"`
    43  
    44  	// The value to use for rel links
    45  	//
    46  	// See https://www.w3schools.com/tags/att_link_rel.asp
    47  	//
    48  	// AMP has a special requirement in this department, see:
    49  	// https://www.ampproject.org/docs/guides/deploy/discovery
    50  	// I.e.:
    51  	// <link rel="amphtml" href="https://www.example.com/url/to/amp/document.html">
    52  	Rel string `json:"rel"`
    53  
    54  	// The protocol to use, i.e. "webcal://". Defaults to the protocol of the baseURL.
    55  	Protocol string `json:"protocol"`
    56  
    57  	// IsPlainText decides whether to use text/template or html/template
    58  	// as template parser.
    59  	IsPlainText bool `json:"isPlainText"`
    60  
    61  	// IsHTML returns whether this format is int the HTML family. This includes
    62  	// HTML, AMP etc. This is used to decide when to create alias redirects etc.
    63  	IsHTML bool `json:"isHTML"`
    64  
    65  	// Enable to ignore the global uglyURLs setting.
    66  	NoUgly bool `json:"noUgly"`
    67  
    68  	// Enable if it doesn't make sense to include this format in an alternative
    69  	// format listing, CSS being one good example.
    70  	// Note that we use the term "alternative" and not "alternate" here, as it
    71  	// does not necessarily replace the other format, it is an alternative representation.
    72  	NotAlternative bool `json:"notAlternative"`
    73  
    74  	// Setting this will make this output format control the value of
    75  	// .Permalink and .RelPermalink for a rendered Page.
    76  	// If not set, these values will point to the main (first) output format
    77  	// configured. That is probably the behaviour you want in most situations,
    78  	// as you probably don't want to link back to the RSS version of a page, as an
    79  	// example. AMP would, however, be a good example of an output format where this
    80  	// behaviour is wanted.
    81  	Permalinkable bool `json:"permalinkable"`
    82  
    83  	// Setting this to a non-zero value will be used as the first sort criteria.
    84  	Weight int `json:"weight"`
    85  }
    86  
    87  // An ordered list of built-in output formats.
    88  var (
    89  	AMPFormat = Format{
    90  		Name:          "AMP",
    91  		MediaType:     media.HTMLType,
    92  		BaseName:      "index",
    93  		Path:          "amp",
    94  		Rel:           "amphtml",
    95  		IsHTML:        true,
    96  		Permalinkable: true,
    97  		// See https://www.ampproject.org/learn/overview/
    98  	}
    99  
   100  	CalendarFormat = Format{
   101  		Name:        "Calendar",
   102  		MediaType:   media.CalendarType,
   103  		IsPlainText: true,
   104  		Protocol:    "webcal://",
   105  		BaseName:    "index",
   106  		Rel:         "alternate",
   107  	}
   108  
   109  	CSSFormat = Format{
   110  		Name:           "CSS",
   111  		MediaType:      media.CSSType,
   112  		BaseName:       "styles",
   113  		IsPlainText:    true,
   114  		Rel:            "stylesheet",
   115  		NotAlternative: true,
   116  	}
   117  	CSVFormat = Format{
   118  		Name:        "CSV",
   119  		MediaType:   media.CSVType,
   120  		BaseName:    "index",
   121  		IsPlainText: true,
   122  		Rel:         "alternate",
   123  	}
   124  
   125  	HTMLFormat = Format{
   126  		Name:          "HTML",
   127  		MediaType:     media.HTMLType,
   128  		BaseName:      "index",
   129  		Rel:           "canonical",
   130  		IsHTML:        true,
   131  		Permalinkable: true,
   132  
   133  		// Weight will be used as first sort criteria. HTML will, by default,
   134  		// be rendered first, but set it to 10 so it's easy to put one above it.
   135  		Weight: 10,
   136  	}
   137  
   138  	JSONFormat = Format{
   139  		Name:        "JSON",
   140  		MediaType:   media.JSONType,
   141  		BaseName:    "index",
   142  		IsPlainText: true,
   143  		Rel:         "alternate",
   144  	}
   145  
   146  	WebAppManifestFormat = Format{
   147  		Name:           "WebAppManifest",
   148  		MediaType:      media.WebAppManifestType,
   149  		BaseName:       "manifest",
   150  		IsPlainText:    true,
   151  		NotAlternative: true,
   152  		Rel:            "manifest",
   153  	}
   154  
   155  	RobotsTxtFormat = Format{
   156  		Name:        "ROBOTS",
   157  		MediaType:   media.TextType,
   158  		BaseName:    "robots",
   159  		IsPlainText: true,
   160  		Rel:         "alternate",
   161  	}
   162  
   163  	RSSFormat = Format{
   164  		Name:      "RSS",
   165  		MediaType: media.RSSType,
   166  		BaseName:  "index",
   167  		NoUgly:    true,
   168  		Rel:       "alternate",
   169  	}
   170  
   171  	SitemapFormat = Format{
   172  		Name:      "Sitemap",
   173  		MediaType: media.XMLType,
   174  		BaseName:  "sitemap",
   175  		NoUgly:    true,
   176  		Rel:       "sitemap",
   177  	}
   178  )
   179  
   180  // DefaultFormats contains the default output formats supported by Hugo.
   181  var DefaultFormats = Formats{
   182  	AMPFormat,
   183  	CalendarFormat,
   184  	CSSFormat,
   185  	CSVFormat,
   186  	HTMLFormat,
   187  	JSONFormat,
   188  	WebAppManifestFormat,
   189  	RobotsTxtFormat,
   190  	RSSFormat,
   191  	SitemapFormat,
   192  }
   193  
   194  func init() {
   195  	sort.Sort(DefaultFormats)
   196  }
   197  
   198  // Formats is a slice of Format.
   199  type Formats []Format
   200  
   201  func (formats Formats) Len() int      { return len(formats) }
   202  func (formats Formats) Swap(i, j int) { formats[i], formats[j] = formats[j], formats[i] }
   203  func (formats Formats) Less(i, j int) bool {
   204  	fi, fj := formats[i], formats[j]
   205  	if fi.Weight == fj.Weight {
   206  		return fi.Name < fj.Name
   207  	}
   208  
   209  	if fj.Weight == 0 {
   210  		return true
   211  	}
   212  
   213  	return fi.Weight > 0 && fi.Weight < fj.Weight
   214  }
   215  
   216  // GetBySuffix gets a output format given as suffix, e.g. "html".
   217  // It will return false if no format could be found, or if the suffix given
   218  // is ambiguous.
   219  // The lookup is case insensitive.
   220  func (formats Formats) GetBySuffix(suffix string) (f Format, found bool) {
   221  	for _, ff := range formats {
   222  		for _, suffix2 := range ff.MediaType.Suffixes() {
   223  			if strings.EqualFold(suffix, suffix2) {
   224  				if found {
   225  					// ambiguous
   226  					found = false
   227  					return
   228  				}
   229  				f = ff
   230  				found = true
   231  			}
   232  		}
   233  	}
   234  	return
   235  }
   236  
   237  // GetByName gets a format by its identifier name.
   238  func (formats Formats) GetByName(name string) (f Format, found bool) {
   239  	for _, ff := range formats {
   240  		if strings.EqualFold(name, ff.Name) {
   241  			f = ff
   242  			found = true
   243  			return
   244  		}
   245  	}
   246  	return
   247  }
   248  
   249  // GetByNames gets a list of formats given a list of identifiers.
   250  func (formats Formats) GetByNames(names ...string) (Formats, error) {
   251  	var types []Format
   252  
   253  	for _, name := range names {
   254  		tpe, ok := formats.GetByName(name)
   255  		if !ok {
   256  			return types, fmt.Errorf("OutputFormat with key %q not found", name)
   257  		}
   258  		types = append(types, tpe)
   259  	}
   260  	return types, nil
   261  }
   262  
   263  // FromFilename gets a Format given a filename.
   264  func (formats Formats) FromFilename(filename string) (f Format, found bool) {
   265  	// mytemplate.amp.html
   266  	// mytemplate.html
   267  	// mytemplate
   268  	var ext, outFormat string
   269  
   270  	parts := strings.Split(filename, ".")
   271  	if len(parts) > 2 {
   272  		outFormat = parts[1]
   273  		ext = parts[2]
   274  	} else if len(parts) > 1 {
   275  		ext = parts[1]
   276  	}
   277  
   278  	if outFormat != "" {
   279  		return formats.GetByName(outFormat)
   280  	}
   281  
   282  	if ext != "" {
   283  		f, found = formats.GetBySuffix(ext)
   284  		if !found && len(parts) == 2 {
   285  			// For extensionless output formats (e.g. Netlify's _redirects)
   286  			// we must fall back to using the extension as format lookup.
   287  			f, found = formats.GetByName(ext)
   288  		}
   289  	}
   290  	return
   291  }
   292  
   293  // DecodeFormats takes a list of output format configurations and merges those,
   294  // in the order given, with the Hugo defaults as the last resort.
   295  func DecodeFormats(mediaTypes media.Types, maps ...map[string]interface{}) (Formats, error) {
   296  	f := make(Formats, len(DefaultFormats))
   297  	copy(f, DefaultFormats)
   298  
   299  	for _, m := range maps {
   300  		for k, v := range m {
   301  			found := false
   302  			for i, vv := range f {
   303  				if strings.EqualFold(k, vv.Name) {
   304  					// Merge it with the existing
   305  					if err := decode(mediaTypes, v, &f[i]); err != nil {
   306  						return f, err
   307  					}
   308  					found = true
   309  				}
   310  			}
   311  			if !found {
   312  				var newOutFormat Format
   313  				newOutFormat.Name = k
   314  				if err := decode(mediaTypes, v, &newOutFormat); err != nil {
   315  					return f, err
   316  				}
   317  
   318  				// We need values for these
   319  				if newOutFormat.BaseName == "" {
   320  					newOutFormat.BaseName = "index"
   321  				}
   322  				if newOutFormat.Rel == "" {
   323  					newOutFormat.Rel = "alternate"
   324  				}
   325  
   326  				f = append(f, newOutFormat)
   327  
   328  			}
   329  		}
   330  	}
   331  
   332  	sort.Sort(f)
   333  
   334  	return f, nil
   335  }
   336  
   337  func decode(mediaTypes media.Types, input interface{}, output *Format) error {
   338  	config := &mapstructure.DecoderConfig{
   339  		Metadata:         nil,
   340  		Result:           output,
   341  		WeaklyTypedInput: true,
   342  		DecodeHook: func(a reflect.Type, b reflect.Type, c interface{}) (interface{}, error) {
   343  			if a.Kind() == reflect.Map {
   344  				dataVal := reflect.Indirect(reflect.ValueOf(c))
   345  				for _, key := range dataVal.MapKeys() {
   346  					keyStr, ok := key.Interface().(string)
   347  					if !ok {
   348  						// Not a string key
   349  						continue
   350  					}
   351  					if strings.EqualFold(keyStr, "mediaType") {
   352  						// If mediaType is a string, look it up and replace it
   353  						// in the map.
   354  						vv := dataVal.MapIndex(key)
   355  						vvi := vv.Interface()
   356  
   357  						switch vviv := vvi.(type) {
   358  						case media.Type:
   359  						// OK
   360  						case string:
   361  							mediaType, found := mediaTypes.GetByType(vviv)
   362  							if !found {
   363  								return c, fmt.Errorf("media type %q not found", vviv)
   364  							}
   365  							dataVal.SetMapIndex(key, reflect.ValueOf(mediaType))
   366  						default:
   367  							return nil, errors.Errorf("invalid output format configuration; wrong type for media type, expected string (e.g. text/html), got %T", vvi)
   368  						}
   369  					}
   370  				}
   371  			}
   372  			return c, nil
   373  		},
   374  	}
   375  
   376  	decoder, err := mapstructure.NewDecoder(config)
   377  	if err != nil {
   378  		return err
   379  	}
   380  
   381  	if err = decoder.Decode(input); err != nil {
   382  		return errors.Wrap(err, "failed to decode output format configuration")
   383  	}
   384  
   385  	return nil
   386  
   387  }
   388  
   389  // BaseFilename returns the base filename of f including an extension (ie.
   390  // "index.xml").
   391  func (f Format) BaseFilename() string {
   392  	return f.BaseName + f.MediaType.FirstSuffix.FullSuffix
   393  }
   394  
   395  // MarshalJSON returns the JSON encoding of f.
   396  func (f Format) MarshalJSON() ([]byte, error) {
   397  	type Alias Format
   398  	return json.Marshal(&struct {
   399  		MediaType string `json:"mediaType"`
   400  		Alias
   401  	}{
   402  		MediaType: f.MediaType.String(),
   403  		Alias:     (Alias)(f),
   404  	})
   405  }