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