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 }