github.com/SDLMoe/hugo@v0.47.1/media/mediaType.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 media
    15  
    16  import (
    17  	"encoding/json"
    18  	"fmt"
    19  	"sort"
    20  	"strings"
    21  
    22  	"github.com/gohugoio/hugo/helpers"
    23  	"github.com/mitchellh/mapstructure"
    24  )
    25  
    26  const (
    27  	defaultDelimiter = "."
    28  )
    29  
    30  // Type (also known as MIME type and content type) is a two-part identifier for
    31  // file formats and format contents transmitted on the Internet.
    32  // For Hugo's use case, we use the top-level type name / subtype name + suffix.
    33  // One example would be application/svg+xml
    34  // If suffix is not provided, the sub type will be used.
    35  // See // https://en.wikipedia.org/wiki/Media_type
    36  type Type struct {
    37  	MainType string `json:"mainType"` // i.e. text
    38  	SubType  string `json:"subType"`  // i.e. html
    39  
    40  	// Deprecated in Hugo 0.44. To be renamed and unexported.
    41  	// Was earlier used both to set file suffix and to augment the MIME type.
    42  	// This had its limitations and issues.
    43  	OldSuffix string `json:"-" mapstructure:"suffix"`
    44  
    45  	Delimiter string `json:"delimiter"` // e.g. "."
    46  
    47  	Suffixes []string `json:"suffixes"`
    48  
    49  	// Set when doing lookup by suffix.
    50  	fileSuffix string
    51  }
    52  
    53  // FromStringAndExt is same as FromString, but adds the file extension to the type.
    54  func FromStringAndExt(t, ext string) (Type, error) {
    55  	tp, err := fromString(t)
    56  	if err != nil {
    57  		return tp, err
    58  	}
    59  	tp.Suffixes = []string{strings.TrimPrefix(ext, ".")}
    60  	return tp, nil
    61  }
    62  
    63  // FromString creates a new Type given a type string on the form MainType/SubType and
    64  // an optional suffix, e.g. "text/html" or "text/html+html".
    65  func fromString(t string) (Type, error) {
    66  	t = strings.ToLower(t)
    67  	parts := strings.Split(t, "/")
    68  	if len(parts) != 2 {
    69  		return Type{}, fmt.Errorf("cannot parse %q as a media type", t)
    70  	}
    71  	mainType := parts[0]
    72  	subParts := strings.Split(parts[1], "+")
    73  
    74  	subType := strings.Split(subParts[0], ";")[0]
    75  
    76  	var suffix string
    77  
    78  	if len(subParts) > 1 {
    79  		suffix = subParts[1]
    80  	}
    81  
    82  	return Type{MainType: mainType, SubType: subType, OldSuffix: suffix}, nil
    83  }
    84  
    85  // Type returns a string representing the main- and sub-type of a media type, e.g. "text/css".
    86  // A suffix identifier will be appended after a "+" if set, e.g. "image/svg+xml".
    87  // Hugo will register a set of default media types.
    88  // These can be overridden by the user in the configuration,
    89  // by defining a media type with the same Type.
    90  func (m Type) Type() string {
    91  	// Examples are
    92  	// image/svg+xml
    93  	// text/css
    94  	if m.OldSuffix != "" {
    95  		return fmt.Sprintf("%s/%s+%s", m.MainType, m.SubType, m.OldSuffix)
    96  	}
    97  	return fmt.Sprintf("%s/%s", m.MainType, m.SubType)
    98  
    99  }
   100  
   101  func (m Type) String() string {
   102  	return m.Type()
   103  }
   104  
   105  // FullSuffix returns the file suffix with any delimiter prepended.
   106  func (m Type) FullSuffix() string {
   107  	return m.Delimiter + m.Suffix()
   108  }
   109  
   110  // Suffix returns the file suffix without any delmiter prepended.
   111  func (m Type) Suffix() string {
   112  	if m.fileSuffix != "" {
   113  		return m.fileSuffix
   114  	}
   115  	if len(m.Suffixes) > 0 {
   116  		return m.Suffixes[0]
   117  	}
   118  	// There are MIME types without file suffixes.
   119  	return ""
   120  }
   121  
   122  var (
   123  	// Definitions from https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types etc.
   124  	// Note that from Hugo 0.44 we only set Suffix if it is part of the MIME type.
   125  	CalendarType   = Type{MainType: "text", SubType: "calendar", Suffixes: []string{"ics"}, Delimiter: defaultDelimiter}
   126  	CSSType        = Type{MainType: "text", SubType: "css", Suffixes: []string{"css"}, Delimiter: defaultDelimiter}
   127  	SCSSType       = Type{MainType: "text", SubType: "x-scss", Suffixes: []string{"scss"}, Delimiter: defaultDelimiter}
   128  	SASSType       = Type{MainType: "text", SubType: "x-sass", Suffixes: []string{"sass"}, Delimiter: defaultDelimiter}
   129  	CSVType        = Type{MainType: "text", SubType: "csv", Suffixes: []string{"csv"}, Delimiter: defaultDelimiter}
   130  	HTMLType       = Type{MainType: "text", SubType: "html", Suffixes: []string{"html"}, Delimiter: defaultDelimiter}
   131  	JavascriptType = Type{MainType: "application", SubType: "javascript", Suffixes: []string{"js"}, Delimiter: defaultDelimiter}
   132  	JSONType       = Type{MainType: "application", SubType: "json", Suffixes: []string{"json"}, Delimiter: defaultDelimiter}
   133  	RSSType        = Type{MainType: "application", SubType: "rss", OldSuffix: "xml", Suffixes: []string{"xml"}, Delimiter: defaultDelimiter}
   134  	XMLType        = Type{MainType: "application", SubType: "xml", Suffixes: []string{"xml"}, Delimiter: defaultDelimiter}
   135  	SVGType        = Type{MainType: "image", SubType: "svg", OldSuffix: "xml", Suffixes: []string{"svg"}, Delimiter: defaultDelimiter}
   136  	TextType       = Type{MainType: "text", SubType: "plain", Suffixes: []string{"txt"}, Delimiter: defaultDelimiter}
   137  
   138  	OctetType = Type{MainType: "application", SubType: "octet-stream"}
   139  )
   140  
   141  var DefaultTypes = Types{
   142  	CalendarType,
   143  	CSSType,
   144  	CSVType,
   145  	SCSSType,
   146  	SASSType,
   147  	HTMLType,
   148  	JavascriptType,
   149  	JSONType,
   150  	RSSType,
   151  	XMLType,
   152  	SVGType,
   153  	TextType,
   154  	OctetType,
   155  }
   156  
   157  func init() {
   158  	sort.Sort(DefaultTypes)
   159  }
   160  
   161  type Types []Type
   162  
   163  func (t Types) Len() int           { return len(t) }
   164  func (t Types) Swap(i, j int)      { t[i], t[j] = t[j], t[i] }
   165  func (t Types) Less(i, j int) bool { return t[i].Type() < t[j].Type() }
   166  
   167  func (t Types) GetByType(tp string) (Type, bool) {
   168  	for _, tt := range t {
   169  		if strings.EqualFold(tt.Type(), tp) {
   170  			return tt, true
   171  		}
   172  	}
   173  
   174  	if !strings.Contains(tp, "+") {
   175  		// Try with the main and sub type
   176  		parts := strings.Split(tp, "/")
   177  		if len(parts) == 2 {
   178  			return t.GetByMainSubType(parts[0], parts[1])
   179  		}
   180  	}
   181  
   182  	return Type{}, false
   183  }
   184  
   185  // GetFirstBySuffix will return the first media type matching the given suffix.
   186  func (t Types) GetFirstBySuffix(suffix string) (Type, bool) {
   187  	for _, tt := range t {
   188  		if match := tt.matchSuffix(suffix); match != "" {
   189  			tt.fileSuffix = match
   190  			return tt, true
   191  		}
   192  	}
   193  	return Type{}, false
   194  }
   195  
   196  // GetBySuffix gets a media type given as suffix, e.g. "html".
   197  // It will return false if no format could be found, or if the suffix given
   198  // is ambiguous.
   199  // The lookup is case insensitive.
   200  func (t Types) GetBySuffix(suffix string) (tp Type, found bool) {
   201  	for _, tt := range t {
   202  		if match := tt.matchSuffix(suffix); match != "" {
   203  			if found {
   204  				// ambiguous
   205  				found = false
   206  				return
   207  			}
   208  			tp = tt
   209  			tp.fileSuffix = match
   210  			found = true
   211  		}
   212  	}
   213  	return
   214  }
   215  
   216  func (t Type) matchSuffix(suffix string) string {
   217  	if strings.EqualFold(suffix, t.OldSuffix) {
   218  		return t.OldSuffix
   219  	}
   220  	for _, s := range t.Suffixes {
   221  		if strings.EqualFold(suffix, s) {
   222  			return s
   223  		}
   224  	}
   225  
   226  	return ""
   227  }
   228  
   229  // GetMainSubType gets a media type given a main and a sub type e.g. "text" and "plain".
   230  // It will return false if no format could be found, or if the combination given
   231  // is ambiguous.
   232  // The lookup is case insensitive.
   233  func (t Types) GetByMainSubType(mainType, subType string) (tp Type, found bool) {
   234  	for _, tt := range t {
   235  		if strings.EqualFold(mainType, tt.MainType) && strings.EqualFold(subType, tt.SubType) {
   236  			if found {
   237  				// ambiguous
   238  				found = false
   239  				return
   240  			}
   241  
   242  			tp = tt
   243  			found = true
   244  		}
   245  	}
   246  	return
   247  }
   248  
   249  func suffixIsDeprecated() {
   250  	helpers.Deprecated("MediaType", "Suffix in config.toml", `
   251  Before Hugo 0.44 this was used both to set a custom file suffix and as way
   252  to augment the mediatype definition (what you see after the "+", e.g. "image/svg+xml").
   253  
   254  This had its limitations. For one, it was only possible with one file extension per MIME type.
   255  
   256  Now you can specify multiple file suffixes using "suffixes", but you need to specify the full MIME type
   257  identifier:
   258  
   259  [mediaTypes]
   260  [mediaTypes."image/svg+xml"]
   261  suffixes = ["svg", "abc" ]
   262  
   263  In most cases, it will be enough to just change:
   264  
   265  [mediaTypes]
   266  [mediaTypes."my/custom-mediatype"]
   267  suffix = "txt"
   268  
   269  To:
   270  
   271  [mediaTypes]
   272  [mediaTypes."my/custom-mediatype"]
   273  suffixes = ["txt"]
   274  
   275  Hugo will still respect values set in "suffix" if no value for "suffixes" is provided, but this will be removed
   276  in a future release.
   277  
   278  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.
   279  `, false)
   280  }
   281  
   282  // DecodeTypes takes a list of media type configurations and merges those,
   283  // in the order given, with the Hugo defaults as the last resort.
   284  func DecodeTypes(maps ...map[string]interface{}) (Types, error) {
   285  	var m Types
   286  
   287  	// Maps type string to Type. Type string is the full application/svg+xml.
   288  	mmm := make(map[string]Type)
   289  	for _, dt := range DefaultTypes {
   290  		suffixes := make([]string, len(dt.Suffixes))
   291  		copy(suffixes, dt.Suffixes)
   292  		dt.Suffixes = suffixes
   293  		mmm[dt.Type()] = dt
   294  	}
   295  
   296  	for _, mm := range maps {
   297  		for k, v := range mm {
   298  			var mediaType Type
   299  
   300  			mediaType, found := mmm[k]
   301  			if !found {
   302  				var err error
   303  				mediaType, err = fromString(k)
   304  				if err != nil {
   305  					return m, err
   306  				}
   307  			}
   308  
   309  			if err := mapstructure.WeakDecode(v, &mediaType); err != nil {
   310  				return m, err
   311  			}
   312  
   313  			vm := v.(map[string]interface{})
   314  			_, delimiterSet := vm["delimiter"]
   315  			_, suffixSet := vm["suffix"]
   316  
   317  			if suffixSet {
   318  				suffixIsDeprecated()
   319  			}
   320  
   321  			// Before Hugo 0.44 we had a non-standard use of the Suffix
   322  			// attribute, and this is now deprecated (use Suffixes for file suffixes).
   323  			// But we need to keep old configurations working for a while.
   324  			if len(mediaType.Suffixes) == 0 && mediaType.OldSuffix != "" {
   325  				mediaType.Suffixes = []string{mediaType.OldSuffix}
   326  			}
   327  			// The user may set the delimiter as an empty string.
   328  			if !delimiterSet && len(mediaType.Suffixes) != 0 {
   329  				mediaType.Delimiter = defaultDelimiter
   330  			} else if suffixSet && !delimiterSet {
   331  				mediaType.Delimiter = defaultDelimiter
   332  			}
   333  
   334  			mmm[k] = mediaType
   335  
   336  		}
   337  	}
   338  
   339  	for _, v := range mmm {
   340  		m = append(m, v)
   341  	}
   342  	sort.Sort(m)
   343  
   344  	return m, nil
   345  }
   346  
   347  func (m Type) MarshalJSON() ([]byte, error) {
   348  	type Alias Type
   349  	return json.Marshal(&struct {
   350  		Type   string `json:"type"`
   351  		String string `json:"string"`
   352  		Alias
   353  	}{
   354  		Type:   m.Type(),
   355  		String: m.String(),
   356  		Alias:  (Alias)(m),
   357  	})
   358  }