github.com/pietrocarrara/hugo@v0.47.1/output/outputFormat.go (about)

     1  // Copyright 2017-present 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  	"sort"
    20  	"strings"
    21  
    22  	"reflect"
    23  
    24  	"github.com/mitchellh/mapstructure"
    25  
    26  	"github.com/gohugoio/hugo/media"
    27  )
    28  
    29  // Format represents an output representation, usually to a file on disk.
    30  type Format struct {
    31  	// The Name is used as an identifier. Internal output formats (i.e. HTML and RSS)
    32  	// can be overridden by providing a new definition for those types.
    33  	Name string `json:"name"`
    34  
    35  	MediaType media.Type `json:"mediaType"`
    36  
    37  	// Must be set to a value when there are two or more conflicting mediatype for the same resource.
    38  	Path string `json:"path"`
    39  
    40  	// The base output file name used when not using "ugly URLs", defaults to "index".
    41  	BaseName string `json:"baseName"`
    42  
    43  	// The value to use for rel links
    44  	//
    45  	// See https://www.w3schools.com/tags/att_link_rel.asp
    46  	//
    47  	// AMP has a special requirement in this department, see:
    48  	// https://www.ampproject.org/docs/guides/deploy/discovery
    49  	// I.e.:
    50  	// <link rel="amphtml" href="https://www.example.com/url/to/amp/document.html">
    51  	Rel string `json:"rel"`
    52  
    53  	// The protocol to use, i.e. "webcal://". Defaults to the protocol of the baseURL.
    54  	Protocol string `json:"protocol"`
    55  
    56  	// IsPlainText decides whether to use text/template or html/template
    57  	// as template parser.
    58  	IsPlainText bool `json:"isPlainText"`
    59  
    60  	// IsHTML returns whether this format is int the HTML family. This includes
    61  	// HTML, AMP etc. This is used to decide when to create alias redirects etc.
    62  	IsHTML bool `json:"isHTML"`
    63  
    64  	// Enable to ignore the global uglyURLs setting.
    65  	NoUgly bool `json:"noUgly"`
    66  
    67  	// Enable if it doesn't make sense to include this format in an alternative
    68  	// format listing, CSS being one good example.
    69  	// Note that we use the term "alternative" and not "alternate" here, as it
    70  	// does not necessarily replace the other format, it is an alternative representation.
    71  	NotAlternative bool `json:"notAlternative"`
    72  }
    73  
    74  var (
    75  	// An ordered list of built-in output formats
    76  	//
    77  	// See https://www.ampproject.org/learn/overview/
    78  	AMPFormat = Format{
    79  		Name:      "AMP",
    80  		MediaType: media.HTMLType,
    81  		BaseName:  "index",
    82  		Path:      "amp",
    83  		Rel:       "amphtml",
    84  		IsHTML:    true,
    85  	}
    86  
    87  	CalendarFormat = Format{
    88  		Name:        "Calendar",
    89  		MediaType:   media.CalendarType,
    90  		IsPlainText: true,
    91  		Protocol:    "webcal://",
    92  		BaseName:    "index",
    93  		Rel:         "alternate",
    94  	}
    95  
    96  	CSSFormat = Format{
    97  		Name:           "CSS",
    98  		MediaType:      media.CSSType,
    99  		BaseName:       "styles",
   100  		IsPlainText:    true,
   101  		Rel:            "stylesheet",
   102  		NotAlternative: true,
   103  	}
   104  	CSVFormat = Format{
   105  		Name:        "CSV",
   106  		MediaType:   media.CSVType,
   107  		BaseName:    "index",
   108  		IsPlainText: true,
   109  		Rel:         "alternate",
   110  	}
   111  
   112  	HTMLFormat = Format{
   113  		Name:      "HTML",
   114  		MediaType: media.HTMLType,
   115  		BaseName:  "index",
   116  		Rel:       "canonical",
   117  		IsHTML:    true,
   118  	}
   119  
   120  	JSONFormat = Format{
   121  		Name:        "JSON",
   122  		MediaType:   media.JSONType,
   123  		BaseName:    "index",
   124  		IsPlainText: true,
   125  		Rel:         "alternate",
   126  	}
   127  
   128  	RobotsTxtFormat = Format{
   129  		Name:        "ROBOTS",
   130  		MediaType:   media.TextType,
   131  		BaseName:    "robots",
   132  		IsPlainText: true,
   133  		Rel:         "alternate",
   134  	}
   135  
   136  	RSSFormat = Format{
   137  		Name:      "RSS",
   138  		MediaType: media.RSSType,
   139  		BaseName:  "index",
   140  		NoUgly:    true,
   141  		Rel:       "alternate",
   142  	}
   143  
   144  	SitemapFormat = Format{
   145  		Name:      "Sitemap",
   146  		MediaType: media.XMLType,
   147  		BaseName:  "sitemap",
   148  		NoUgly:    true,
   149  		Rel:       "sitemap",
   150  	}
   151  )
   152  
   153  var DefaultFormats = Formats{
   154  	AMPFormat,
   155  	CalendarFormat,
   156  	CSSFormat,
   157  	CSVFormat,
   158  	HTMLFormat,
   159  	JSONFormat,
   160  	RobotsTxtFormat,
   161  	RSSFormat,
   162  	SitemapFormat,
   163  }
   164  
   165  func init() {
   166  	sort.Sort(DefaultFormats)
   167  }
   168  
   169  type Formats []Format
   170  
   171  func (formats Formats) Len() int           { return len(formats) }
   172  func (formats Formats) Swap(i, j int)      { formats[i], formats[j] = formats[j], formats[i] }
   173  func (formats Formats) Less(i, j int) bool { return formats[i].Name < formats[j].Name }
   174  
   175  // GetBySuffix gets a output format given as suffix, e.g. "html".
   176  // It will return false if no format could be found, or if the suffix given
   177  // is ambiguous.
   178  // The lookup is case insensitive.
   179  func (formats Formats) GetBySuffix(suffix string) (f Format, found bool) {
   180  	for _, ff := range formats {
   181  		if strings.EqualFold(suffix, ff.MediaType.Suffix()) {
   182  			if found {
   183  				// ambiguous
   184  				found = false
   185  				return
   186  			}
   187  			f = ff
   188  			found = true
   189  		}
   190  	}
   191  	return
   192  }
   193  
   194  // GetByName gets a format by its identifier name.
   195  func (formats Formats) GetByName(name string) (f Format, found bool) {
   196  	for _, ff := range formats {
   197  		if strings.EqualFold(name, ff.Name) {
   198  			f = ff
   199  			found = true
   200  			return
   201  		}
   202  	}
   203  	return
   204  }
   205  
   206  // GetByNames gets a list of formats given a list of identifiers.
   207  func (formats Formats) GetByNames(names ...string) (Formats, error) {
   208  	var types []Format
   209  
   210  	for _, name := range names {
   211  		tpe, ok := formats.GetByName(name)
   212  		if !ok {
   213  			return types, fmt.Errorf("OutputFormat with key %q not found", name)
   214  		}
   215  		types = append(types, tpe)
   216  	}
   217  	return types, nil
   218  }
   219  
   220  // FromFilename gets a Format given a filename.
   221  func (formats Formats) FromFilename(filename string) (f Format, found bool) {
   222  	// mytemplate.amp.html
   223  	// mytemplate.html
   224  	// mytemplate
   225  	var ext, outFormat string
   226  
   227  	parts := strings.Split(filename, ".")
   228  	if len(parts) > 2 {
   229  		outFormat = parts[1]
   230  		ext = parts[2]
   231  	} else if len(parts) > 1 {
   232  		ext = parts[1]
   233  	}
   234  
   235  	if outFormat != "" {
   236  		return formats.GetByName(outFormat)
   237  	}
   238  
   239  	if ext != "" {
   240  		f, found = formats.GetBySuffix(ext)
   241  		if !found && len(parts) == 2 {
   242  			// For extensionless output formats (e.g. Netlify's _redirects)
   243  			// we must fall back to using the extension as format lookup.
   244  			f, found = formats.GetByName(ext)
   245  		}
   246  	}
   247  	return
   248  }
   249  
   250  // DecodeFormats takes a list of output format configurations and merges those,
   251  // in the order given, with the Hugo defaults as the last resort.
   252  func DecodeFormats(mediaTypes media.Types, maps ...map[string]interface{}) (Formats, error) {
   253  	f := make(Formats, len(DefaultFormats))
   254  	copy(f, DefaultFormats)
   255  
   256  	for _, m := range maps {
   257  		for k, v := range m {
   258  			found := false
   259  			for i, vv := range f {
   260  				if strings.EqualFold(k, vv.Name) {
   261  					// Merge it with the existing
   262  					if err := decode(mediaTypes, v, &f[i]); err != nil {
   263  						return f, err
   264  					}
   265  					found = true
   266  				}
   267  			}
   268  			if !found {
   269  				var newOutFormat Format
   270  				newOutFormat.Name = k
   271  				if err := decode(mediaTypes, v, &newOutFormat); err != nil {
   272  					return f, err
   273  				}
   274  
   275  				// We need values for these
   276  				if newOutFormat.BaseName == "" {
   277  					newOutFormat.BaseName = "index"
   278  				}
   279  				if newOutFormat.Rel == "" {
   280  					newOutFormat.Rel = "alternate"
   281  				}
   282  
   283  				f = append(f, newOutFormat)
   284  			}
   285  		}
   286  	}
   287  
   288  	sort.Sort(f)
   289  
   290  	return f, nil
   291  }
   292  
   293  func decode(mediaTypes media.Types, input, output interface{}) error {
   294  	config := &mapstructure.DecoderConfig{
   295  		Metadata:         nil,
   296  		Result:           output,
   297  		WeaklyTypedInput: true,
   298  		DecodeHook: func(a reflect.Type, b reflect.Type, c interface{}) (interface{}, error) {
   299  			if a.Kind() == reflect.Map {
   300  				dataVal := reflect.Indirect(reflect.ValueOf(c))
   301  				for _, key := range dataVal.MapKeys() {
   302  					keyStr, ok := key.Interface().(string)
   303  					if !ok {
   304  						// Not a string key
   305  						continue
   306  					}
   307  					if strings.EqualFold(keyStr, "mediaType") {
   308  						// If mediaType is a string, look it up and replace it
   309  						// in the map.
   310  						vv := dataVal.MapIndex(key)
   311  						if mediaTypeStr, ok := vv.Interface().(string); ok {
   312  							mediaType, found := mediaTypes.GetByType(mediaTypeStr)
   313  							if !found {
   314  								return c, fmt.Errorf("media type %q not found", mediaTypeStr)
   315  							}
   316  							dataVal.SetMapIndex(key, reflect.ValueOf(mediaType))
   317  						}
   318  					}
   319  				}
   320  			}
   321  			return c, nil
   322  		},
   323  	}
   324  
   325  	decoder, err := mapstructure.NewDecoder(config)
   326  	if err != nil {
   327  		return err
   328  	}
   329  
   330  	return decoder.Decode(input)
   331  }
   332  
   333  func (formats Format) BaseFilename() string {
   334  	return formats.BaseName + formats.MediaType.FullSuffix()
   335  }
   336  
   337  func (formats Format) MarshalJSON() ([]byte, error) {
   338  	type Alias Format
   339  	return json.Marshal(&struct {
   340  		MediaType string
   341  		Alias
   342  	}{
   343  		MediaType: formats.MediaType.String(),
   344  		Alias:     (Alias)(formats),
   345  	})
   346  }