github.com/pietrocarrara/hugo@v0.47.1/output/outputFormat.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 output 15 16 import ( 17 "encoding/json" 18 "fmt" 19 "sort" 20 "strings" 21 22 "reflect" 23 24 "github.com/mitchellh/mapstructure" 25 26 "github.com/gohugoio/hugo/media" 27 ) 28 29 // Format represents an output representation, usually to a file on disk. 30 type Format struct { 31 // The Name is used as an identifier. Internal output formats (i.e. HTML and RSS) 32 // can be overridden by providing a new definition for those types. 33 Name string `json:"name"` 34 35 MediaType media.Type `json:"mediaType"` 36 37 // Must be set to a value when there are two or more conflicting mediatype for the same resource. 38 Path string `json:"path"` 39 40 // The base output file name used when not using "ugly URLs", defaults to "index". 41 BaseName string `json:"baseName"` 42 43 // The value to use for rel links 44 // 45 // See https://www.w3schools.com/tags/att_link_rel.asp 46 // 47 // AMP has a special requirement in this department, see: 48 // https://www.ampproject.org/docs/guides/deploy/discovery 49 // I.e.: 50 // <link rel="amphtml" href="https://www.example.com/url/to/amp/document.html"> 51 Rel string `json:"rel"` 52 53 // The protocol to use, i.e. "webcal://". Defaults to the protocol of the baseURL. 54 Protocol string `json:"protocol"` 55 56 // IsPlainText decides whether to use text/template or html/template 57 // as template parser. 58 IsPlainText bool `json:"isPlainText"` 59 60 // IsHTML returns whether this format is int the HTML family. This includes 61 // HTML, AMP etc. This is used to decide when to create alias redirects etc. 62 IsHTML bool `json:"isHTML"` 63 64 // Enable to ignore the global uglyURLs setting. 65 NoUgly bool `json:"noUgly"` 66 67 // Enable if it doesn't make sense to include this format in an alternative 68 // format listing, CSS being one good example. 69 // Note that we use the term "alternative" and not "alternate" here, as it 70 // does not necessarily replace the other format, it is an alternative representation. 71 NotAlternative bool `json:"notAlternative"` 72 } 73 74 var ( 75 // An ordered list of built-in output formats 76 // 77 // See https://www.ampproject.org/learn/overview/ 78 AMPFormat = Format{ 79 Name: "AMP", 80 MediaType: media.HTMLType, 81 BaseName: "index", 82 Path: "amp", 83 Rel: "amphtml", 84 IsHTML: true, 85 } 86 87 CalendarFormat = Format{ 88 Name: "Calendar", 89 MediaType: media.CalendarType, 90 IsPlainText: true, 91 Protocol: "webcal://", 92 BaseName: "index", 93 Rel: "alternate", 94 } 95 96 CSSFormat = Format{ 97 Name: "CSS", 98 MediaType: media.CSSType, 99 BaseName: "styles", 100 IsPlainText: true, 101 Rel: "stylesheet", 102 NotAlternative: true, 103 } 104 CSVFormat = Format{ 105 Name: "CSV", 106 MediaType: media.CSVType, 107 BaseName: "index", 108 IsPlainText: true, 109 Rel: "alternate", 110 } 111 112 HTMLFormat = Format{ 113 Name: "HTML", 114 MediaType: media.HTMLType, 115 BaseName: "index", 116 Rel: "canonical", 117 IsHTML: true, 118 } 119 120 JSONFormat = Format{ 121 Name: "JSON", 122 MediaType: media.JSONType, 123 BaseName: "index", 124 IsPlainText: true, 125 Rel: "alternate", 126 } 127 128 RobotsTxtFormat = Format{ 129 Name: "ROBOTS", 130 MediaType: media.TextType, 131 BaseName: "robots", 132 IsPlainText: true, 133 Rel: "alternate", 134 } 135 136 RSSFormat = Format{ 137 Name: "RSS", 138 MediaType: media.RSSType, 139 BaseName: "index", 140 NoUgly: true, 141 Rel: "alternate", 142 } 143 144 SitemapFormat = Format{ 145 Name: "Sitemap", 146 MediaType: media.XMLType, 147 BaseName: "sitemap", 148 NoUgly: true, 149 Rel: "sitemap", 150 } 151 ) 152 153 var DefaultFormats = Formats{ 154 AMPFormat, 155 CalendarFormat, 156 CSSFormat, 157 CSVFormat, 158 HTMLFormat, 159 JSONFormat, 160 RobotsTxtFormat, 161 RSSFormat, 162 SitemapFormat, 163 } 164 165 func init() { 166 sort.Sort(DefaultFormats) 167 } 168 169 type Formats []Format 170 171 func (formats Formats) Len() int { return len(formats) } 172 func (formats Formats) Swap(i, j int) { formats[i], formats[j] = formats[j], formats[i] } 173 func (formats Formats) Less(i, j int) bool { return formats[i].Name < formats[j].Name } 174 175 // GetBySuffix gets a output format given as suffix, e.g. "html". 176 // It will return false if no format could be found, or if the suffix given 177 // is ambiguous. 178 // The lookup is case insensitive. 179 func (formats Formats) GetBySuffix(suffix string) (f Format, found bool) { 180 for _, ff := range formats { 181 if strings.EqualFold(suffix, ff.MediaType.Suffix()) { 182 if found { 183 // ambiguous 184 found = false 185 return 186 } 187 f = ff 188 found = true 189 } 190 } 191 return 192 } 193 194 // GetByName gets a format by its identifier name. 195 func (formats Formats) GetByName(name string) (f Format, found bool) { 196 for _, ff := range formats { 197 if strings.EqualFold(name, ff.Name) { 198 f = ff 199 found = true 200 return 201 } 202 } 203 return 204 } 205 206 // GetByNames gets a list of formats given a list of identifiers. 207 func (formats Formats) GetByNames(names ...string) (Formats, error) { 208 var types []Format 209 210 for _, name := range names { 211 tpe, ok := formats.GetByName(name) 212 if !ok { 213 return types, fmt.Errorf("OutputFormat with key %q not found", name) 214 } 215 types = append(types, tpe) 216 } 217 return types, nil 218 } 219 220 // FromFilename gets a Format given a filename. 221 func (formats Formats) FromFilename(filename string) (f Format, found bool) { 222 // mytemplate.amp.html 223 // mytemplate.html 224 // mytemplate 225 var ext, outFormat string 226 227 parts := strings.Split(filename, ".") 228 if len(parts) > 2 { 229 outFormat = parts[1] 230 ext = parts[2] 231 } else if len(parts) > 1 { 232 ext = parts[1] 233 } 234 235 if outFormat != "" { 236 return formats.GetByName(outFormat) 237 } 238 239 if ext != "" { 240 f, found = formats.GetBySuffix(ext) 241 if !found && len(parts) == 2 { 242 // For extensionless output formats (e.g. Netlify's _redirects) 243 // we must fall back to using the extension as format lookup. 244 f, found = formats.GetByName(ext) 245 } 246 } 247 return 248 } 249 250 // DecodeFormats takes a list of output format configurations and merges those, 251 // in the order given, with the Hugo defaults as the last resort. 252 func DecodeFormats(mediaTypes media.Types, maps ...map[string]interface{}) (Formats, error) { 253 f := make(Formats, len(DefaultFormats)) 254 copy(f, DefaultFormats) 255 256 for _, m := range maps { 257 for k, v := range m { 258 found := false 259 for i, vv := range f { 260 if strings.EqualFold(k, vv.Name) { 261 // Merge it with the existing 262 if err := decode(mediaTypes, v, &f[i]); err != nil { 263 return f, err 264 } 265 found = true 266 } 267 } 268 if !found { 269 var newOutFormat Format 270 newOutFormat.Name = k 271 if err := decode(mediaTypes, v, &newOutFormat); err != nil { 272 return f, err 273 } 274 275 // We need values for these 276 if newOutFormat.BaseName == "" { 277 newOutFormat.BaseName = "index" 278 } 279 if newOutFormat.Rel == "" { 280 newOutFormat.Rel = "alternate" 281 } 282 283 f = append(f, newOutFormat) 284 } 285 } 286 } 287 288 sort.Sort(f) 289 290 return f, nil 291 } 292 293 func decode(mediaTypes media.Types, input, output interface{}) error { 294 config := &mapstructure.DecoderConfig{ 295 Metadata: nil, 296 Result: output, 297 WeaklyTypedInput: true, 298 DecodeHook: func(a reflect.Type, b reflect.Type, c interface{}) (interface{}, error) { 299 if a.Kind() == reflect.Map { 300 dataVal := reflect.Indirect(reflect.ValueOf(c)) 301 for _, key := range dataVal.MapKeys() { 302 keyStr, ok := key.Interface().(string) 303 if !ok { 304 // Not a string key 305 continue 306 } 307 if strings.EqualFold(keyStr, "mediaType") { 308 // If mediaType is a string, look it up and replace it 309 // in the map. 310 vv := dataVal.MapIndex(key) 311 if mediaTypeStr, ok := vv.Interface().(string); ok { 312 mediaType, found := mediaTypes.GetByType(mediaTypeStr) 313 if !found { 314 return c, fmt.Errorf("media type %q not found", mediaTypeStr) 315 } 316 dataVal.SetMapIndex(key, reflect.ValueOf(mediaType)) 317 } 318 } 319 } 320 } 321 return c, nil 322 }, 323 } 324 325 decoder, err := mapstructure.NewDecoder(config) 326 if err != nil { 327 return err 328 } 329 330 return decoder.Decode(input) 331 } 332 333 func (formats Format) BaseFilename() string { 334 return formats.BaseName + formats.MediaType.FullSuffix() 335 } 336 337 func (formats Format) MarshalJSON() ([]byte, error) { 338 type Alias Format 339 return json.Marshal(&struct { 340 MediaType string 341 Alias 342 }{ 343 MediaType: formats.MediaType.String(), 344 Alias: (Alias)(formats), 345 }) 346 }