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 }