github.com/gohugoio/hugo@v0.88.1/media/mediaType.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 media
    15  
    16  import (
    17  	"encoding/json"
    18  	"errors"
    19  	"fmt"
    20  	"sort"
    21  	"strings"
    22  
    23  	"github.com/spf13/cast"
    24  
    25  	"github.com/gohugoio/hugo/common/maps"
    26  
    27  	"github.com/mitchellh/mapstructure"
    28  )
    29  
    30  const (
    31  	defaultDelimiter = "."
    32  )
    33  
    34  // Type (also known as MIME type and content type) is a two-part identifier for
    35  // file formats and format contents transmitted on the Internet.
    36  // For Hugo's use case, we use the top-level type name / subtype name + suffix.
    37  // One example would be application/svg+xml
    38  // If suffix is not provided, the sub type will be used.
    39  // See // https://en.wikipedia.org/wiki/Media_type
    40  type Type struct {
    41  	MainType  string `json:"mainType"`  // i.e. text
    42  	SubType   string `json:"subType"`   // i.e. html
    43  	Delimiter string `json:"delimiter"` // e.g. "."
    44  
    45  	// FirstSuffix holds the first suffix defined for this Type.
    46  	FirstSuffix SuffixInfo `json:"firstSuffix"`
    47  
    48  	// This is the optional suffix after the "+" in the MIME type,
    49  	//  e.g. "xml" in "application/rss+xml".
    50  	mimeSuffix string
    51  
    52  	// E.g. "jpg,jpeg"
    53  	// Stored as a string to make Type comparable.
    54  	suffixesCSV string
    55  }
    56  
    57  // SuffixInfo holds information about a Type's suffix.
    58  type SuffixInfo struct {
    59  	Suffix     string `json:"suffix"`
    60  	FullSuffix string `json:"fullSuffix"`
    61  }
    62  
    63  // FromStringAndExt creates a Type from a MIME string and a given extension.
    64  func FromStringAndExt(t, ext string) (Type, error) {
    65  	tp, err := fromString(t)
    66  	if err != nil {
    67  		return tp, err
    68  	}
    69  	tp.suffixesCSV = strings.TrimPrefix(ext, ".")
    70  	tp.Delimiter = defaultDelimiter
    71  	tp.init()
    72  	return tp, nil
    73  }
    74  
    75  // FromString creates a new Type given a type string on the form MainType/SubType and
    76  // an optional suffix, e.g. "text/html" or "text/html+html".
    77  func fromString(t string) (Type, error) {
    78  	t = strings.ToLower(t)
    79  	parts := strings.Split(t, "/")
    80  	if len(parts) != 2 {
    81  		return Type{}, fmt.Errorf("cannot parse %q as a media type", t)
    82  	}
    83  	mainType := parts[0]
    84  	subParts := strings.Split(parts[1], "+")
    85  
    86  	subType := strings.Split(subParts[0], ";")[0]
    87  
    88  	var suffix string
    89  
    90  	if len(subParts) > 1 {
    91  		suffix = subParts[1]
    92  	}
    93  
    94  	return Type{MainType: mainType, SubType: subType, mimeSuffix: suffix}, nil
    95  }
    96  
    97  // Type returns a string representing the main- and sub-type of a media type, e.g. "text/css".
    98  // A suffix identifier will be appended after a "+" if set, e.g. "image/svg+xml".
    99  // Hugo will register a set of default media types.
   100  // These can be overridden by the user in the configuration,
   101  // by defining a media type with the same Type.
   102  func (m Type) Type() string {
   103  	// Examples are
   104  	// image/svg+xml
   105  	// text/css
   106  	if m.mimeSuffix != "" {
   107  		return m.MainType + "/" + m.SubType + "+" + m.mimeSuffix
   108  	}
   109  	return m.MainType + "/" + m.SubType
   110  }
   111  
   112  func (m Type) String() string {
   113  	return m.Type()
   114  }
   115  
   116  // Suffixes returns all valid file suffixes for this type.
   117  func (m Type) Suffixes() []string {
   118  	if m.suffixesCSV == "" {
   119  		return nil
   120  	}
   121  
   122  	return strings.Split(m.suffixesCSV, ",")
   123  }
   124  
   125  func (m *Type) init() {
   126  	m.FirstSuffix.FullSuffix = ""
   127  	m.FirstSuffix.Suffix = ""
   128  	if suffixes := m.Suffixes(); suffixes != nil {
   129  		m.FirstSuffix.Suffix = suffixes[0]
   130  		m.FirstSuffix.FullSuffix = m.Delimiter + m.FirstSuffix.Suffix
   131  	}
   132  }
   133  
   134  // WithDelimiterAndSuffixes is used in tests.
   135  func WithDelimiterAndSuffixes(t Type, delimiter, suffixesCSV string) Type {
   136  	t.Delimiter = delimiter
   137  	t.suffixesCSV = suffixesCSV
   138  	t.init()
   139  	return t
   140  }
   141  
   142  func newMediaType(main, sub string, suffixes []string) Type {
   143  	t := Type{MainType: main, SubType: sub, suffixesCSV: strings.Join(suffixes, ","), Delimiter: defaultDelimiter}
   144  	t.init()
   145  	return t
   146  }
   147  
   148  func newMediaTypeWithMimeSuffix(main, sub, mimeSuffix string, suffixes []string) Type {
   149  	mt := newMediaType(main, sub, suffixes)
   150  	mt.mimeSuffix = mimeSuffix
   151  	mt.init()
   152  	return mt
   153  }
   154  
   155  // Definitions from https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types etc.
   156  // Note that from Hugo 0.44 we only set Suffix if it is part of the MIME type.
   157  var (
   158  	CalendarType   = newMediaType("text", "calendar", []string{"ics"})
   159  	CSSType        = newMediaType("text", "css", []string{"css"})
   160  	SCSSType       = newMediaType("text", "x-scss", []string{"scss"})
   161  	SASSType       = newMediaType("text", "x-sass", []string{"sass"})
   162  	CSVType        = newMediaType("text", "csv", []string{"csv"})
   163  	HTMLType       = newMediaType("text", "html", []string{"html"})
   164  	JavascriptType = newMediaType("application", "javascript", []string{"js"})
   165  	TypeScriptType = newMediaType("application", "typescript", []string{"ts"})
   166  	TSXType        = newMediaType("text", "tsx", []string{"tsx"})
   167  	JSXType        = newMediaType("text", "jsx", []string{"jsx"})
   168  
   169  	JSONType           = newMediaType("application", "json", []string{"json"})
   170  	WebAppManifestType = newMediaTypeWithMimeSuffix("application", "manifest", "json", []string{"webmanifest"})
   171  	RSSType            = newMediaTypeWithMimeSuffix("application", "rss", "xml", []string{"xml"})
   172  	XMLType            = newMediaType("application", "xml", []string{"xml"})
   173  	SVGType            = newMediaTypeWithMimeSuffix("image", "svg", "xml", []string{"svg"})
   174  	TextType           = newMediaType("text", "plain", []string{"txt"})
   175  	TOMLType           = newMediaType("application", "toml", []string{"toml"})
   176  	YAMLType           = newMediaType("application", "yaml", []string{"yaml", "yml"})
   177  
   178  	// Common image types
   179  	PNGType  = newMediaType("image", "png", []string{"png"})
   180  	JPEGType = newMediaType("image", "jpeg", []string{"jpg", "jpeg"})
   181  	GIFType  = newMediaType("image", "gif", []string{"gif"})
   182  	TIFFType = newMediaType("image", "tiff", []string{"tif", "tiff"})
   183  	BMPType  = newMediaType("image", "bmp", []string{"bmp"})
   184  	WEBPType = newMediaType("image", "webp", []string{"webp"})
   185  
   186  	// Common video types
   187  	AVIType  = newMediaType("video", "x-msvideo", []string{"avi"})
   188  	MPEGType = newMediaType("video", "mpeg", []string{"mpg", "mpeg"})
   189  	MP4Type  = newMediaType("video", "mp4", []string{"mp4"})
   190  	OGGType  = newMediaType("video", "ogg", []string{"ogv"})
   191  	WEBMType = newMediaType("video", "webm", []string{"webm"})
   192  	GPPType  = newMediaType("video", "3gpp", []string{"3gpp", "3gp"})
   193  
   194  	OctetType = newMediaType("application", "octet-stream", nil)
   195  )
   196  
   197  // DefaultTypes is the default media types supported by Hugo.
   198  var DefaultTypes = Types{
   199  	CalendarType,
   200  	CSSType,
   201  	CSVType,
   202  	SCSSType,
   203  	SASSType,
   204  	HTMLType,
   205  	JavascriptType,
   206  	TypeScriptType,
   207  	TSXType,
   208  	JSXType,
   209  	JSONType,
   210  	WebAppManifestType,
   211  	RSSType,
   212  	XMLType,
   213  	SVGType,
   214  	TextType,
   215  	OctetType,
   216  	YAMLType,
   217  	TOMLType,
   218  	PNGType,
   219  	JPEGType,
   220  	WEBPType,
   221  	AVIType,
   222  	MPEGType,
   223  	MP4Type,
   224  	OGGType,
   225  	WEBMType,
   226  	GPPType,
   227  }
   228  
   229  func init() {
   230  	sort.Sort(DefaultTypes)
   231  }
   232  
   233  // Types is a slice of media types.
   234  type Types []Type
   235  
   236  func (t Types) Len() int           { return len(t) }
   237  func (t Types) Swap(i, j int)      { t[i], t[j] = t[j], t[i] }
   238  func (t Types) Less(i, j int) bool { return t[i].Type() < t[j].Type() }
   239  
   240  // GetByType returns a media type for tp.
   241  func (t Types) GetByType(tp string) (Type, bool) {
   242  	for _, tt := range t {
   243  		if strings.EqualFold(tt.Type(), tp) {
   244  			return tt, true
   245  		}
   246  	}
   247  
   248  	if !strings.Contains(tp, "+") {
   249  		// Try with the main and sub type
   250  		parts := strings.Split(tp, "/")
   251  		if len(parts) == 2 {
   252  			return t.GetByMainSubType(parts[0], parts[1])
   253  		}
   254  	}
   255  
   256  	return Type{}, false
   257  }
   258  
   259  // BySuffix will return all media types matching a suffix.
   260  func (t Types) BySuffix(suffix string) []Type {
   261  	suffix = strings.ToLower(suffix)
   262  	var types []Type
   263  	for _, tt := range t {
   264  		if tt.hasSuffix(suffix) {
   265  			types = append(types, tt)
   266  		}
   267  	}
   268  	return types
   269  }
   270  
   271  // GetFirstBySuffix will return the first type matching the given suffix.
   272  func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) {
   273  	suffix = strings.ToLower(suffix)
   274  	for _, tt := range t {
   275  		if tt.hasSuffix(suffix) {
   276  			return tt, SuffixInfo{
   277  				FullSuffix: tt.Delimiter + suffix,
   278  				Suffix:     suffix,
   279  			}, true
   280  		}
   281  	}
   282  	return Type{}, SuffixInfo{}, false
   283  }
   284  
   285  // GetBySuffix gets a media type given as suffix, e.g. "html".
   286  // It will return false if no format could be found, or if the suffix given
   287  // is ambiguous.
   288  // The lookup is case insensitive.
   289  func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
   290  	suffix = strings.ToLower(suffix)
   291  	for _, tt := range t {
   292  		if tt.hasSuffix(suffix) {
   293  			if found {
   294  				// ambiguous
   295  				found = false
   296  				return
   297  			}
   298  			tp = tt
   299  			si = SuffixInfo{
   300  				FullSuffix: tt.Delimiter + suffix,
   301  				Suffix:     suffix,
   302  			}
   303  			found = true
   304  		}
   305  	}
   306  	return
   307  }
   308  
   309  func (m Type) hasSuffix(suffix string) bool {
   310  	return strings.Contains(","+m.suffixesCSV+",", ","+suffix+",")
   311  }
   312  
   313  // GetByMainSubType gets a media type given a main and a sub type e.g. "text" and "plain".
   314  // It will return false if no format could be found, or if the combination given
   315  // is ambiguous.
   316  // The lookup is case insensitive.
   317  func (t Types) GetByMainSubType(mainType, subType string) (tp Type, found bool) {
   318  	for _, tt := range t {
   319  		if strings.EqualFold(mainType, tt.MainType) && strings.EqualFold(subType, tt.SubType) {
   320  			if found {
   321  				// ambiguous
   322  				found = false
   323  				return
   324  			}
   325  
   326  			tp = tt
   327  			found = true
   328  		}
   329  	}
   330  	return
   331  }
   332  
   333  func suffixIsRemoved() error {
   334  	return errors.New(`MediaType.Suffix is removed. Before Hugo 0.44 this was used both to set a custom file suffix and as way
   335  to augment the mediatype definition (what you see after the "+", e.g. "image/svg+xml").
   336  
   337  This had its limitations. For one, it was only possible with one file extension per MIME type.
   338  
   339  Now you can specify multiple file suffixes using "suffixes", but you need to specify the full MIME type
   340  identifier:
   341  
   342  [mediaTypes]
   343  [mediaTypes."image/svg+xml"]
   344  suffixes = ["svg", "abc" ]
   345  
   346  In most cases, it will be enough to just change:
   347  
   348  [mediaTypes]
   349  [mediaTypes."my/custom-mediatype"]
   350  suffix = "txt"
   351  
   352  To:
   353  
   354  [mediaTypes]
   355  [mediaTypes."my/custom-mediatype"]
   356  suffixes = ["txt"]
   357  
   358  Note that you can still get the Media Type's suffix from a template: {{ $mediaType.Suffix }}. But this will now map to the MIME type filename.
   359  `)
   360  }
   361  
   362  // DecodeTypes takes a list of media type configurations and merges those,
   363  // in the order given, with the Hugo defaults as the last resort.
   364  func DecodeTypes(mms ...map[string]interface{}) (Types, error) {
   365  	var m Types
   366  
   367  	// Maps type string to Type. Type string is the full application/svg+xml.
   368  	mmm := make(map[string]Type)
   369  	for _, dt := range DefaultTypes {
   370  		mmm[dt.Type()] = dt
   371  	}
   372  
   373  	for _, mm := range mms {
   374  		for k, v := range mm {
   375  			var mediaType Type
   376  
   377  			mediaType, found := mmm[k]
   378  			if !found {
   379  				var err error
   380  				mediaType, err = fromString(k)
   381  				if err != nil {
   382  					return m, err
   383  				}
   384  			}
   385  
   386  			if err := mapstructure.WeakDecode(v, &mediaType); err != nil {
   387  				return m, err
   388  			}
   389  
   390  			vm := maps.ToStringMap(v)
   391  			maps.PrepareParams(vm)
   392  			_, delimiterSet := vm["delimiter"]
   393  			_, suffixSet := vm["suffix"]
   394  
   395  			if suffixSet {
   396  				return Types{}, suffixIsRemoved()
   397  			}
   398  
   399  			if suffixes, found := vm["suffixes"]; found {
   400  				mediaType.suffixesCSV = strings.TrimSpace(strings.ToLower(strings.Join(cast.ToStringSlice(suffixes), ",")))
   401  			}
   402  
   403  			// The user may set the delimiter as an empty string.
   404  			if !delimiterSet && mediaType.suffixesCSV != "" {
   405  				mediaType.Delimiter = defaultDelimiter
   406  			}
   407  
   408  			mediaType.init()
   409  
   410  			mmm[k] = mediaType
   411  
   412  		}
   413  	}
   414  
   415  	for _, v := range mmm {
   416  		m = append(m, v)
   417  	}
   418  	sort.Sort(m)
   419  
   420  	return m, nil
   421  }
   422  
   423  // IsZero reports whether this Type represents a zero value.
   424  func (m Type) IsZero() bool {
   425  	return m.SubType == ""
   426  }
   427  
   428  // MarshalJSON returns the JSON encoding of m.
   429  func (m Type) MarshalJSON() ([]byte, error) {
   430  	type Alias Type
   431  	return json.Marshal(&struct {
   432  		Alias
   433  		Type     string   `json:"type"`
   434  		String   string   `json:"string"`
   435  		Suffixes []string `json:"suffixes"`
   436  	}{
   437  		Alias:    (Alias)(m),
   438  		Type:     m.Type(),
   439  		String:   m.String(),
   440  		Suffixes: strings.Split(m.suffixesCSV, ","),
   441  	})
   442  }