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 }