hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

outputFormat.go (10760B)

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