github.com/whatlly/hugo@v0.47.1/parser/frontmatter.go (about) 1 // Copyright 2015 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 parser 15 16 // TODO(bep) archetype remove unused from this package. 17 18 import ( 19 "bytes" 20 "encoding/json" 21 "errors" 22 "fmt" 23 "io" 24 "strings" 25 26 "github.com/gohugoio/hugo/helpers" 27 28 "github.com/spf13/cast" 29 30 "github.com/BurntSushi/toml" 31 "github.com/chaseadamsio/goorgeous" 32 33 "gopkg.in/yaml.v2" 34 ) 35 36 // FrontmatterType represents a type of frontmatter. 37 type FrontmatterType struct { 38 // Parse decodes content into a Go interface. 39 Parse func([]byte) (map[string]interface{}, error) 40 41 markstart, markend []byte // starting and ending delimiters 42 includeMark bool // include start and end mark in output 43 } 44 45 // InterfaceToConfig encodes a given input based upon the mark and writes to w. 46 func InterfaceToConfig(in interface{}, mark rune, w io.Writer) error { 47 if in == nil { 48 return errors.New("input was nil") 49 } 50 51 switch mark { 52 case rune(YAMLLead[0]): 53 b, err := yaml.Marshal(in) 54 if err != nil { 55 return err 56 } 57 58 _, err = w.Write(b) 59 return err 60 61 case rune(TOMLLead[0]): 62 return toml.NewEncoder(w).Encode(in) 63 case rune(JSONLead[0]): 64 b, err := json.MarshalIndent(in, "", " ") 65 if err != nil { 66 return err 67 } 68 69 _, err = w.Write(b) 70 if err != nil { 71 return err 72 } 73 74 _, err = w.Write([]byte{'\n'}) 75 return err 76 77 default: 78 return errors.New("Unsupported Format provided") 79 } 80 } 81 82 // InterfaceToFrontMatter encodes a given input into a frontmatter 83 // representation based upon the mark with the appropriate front matter delimiters 84 // surrounding the output, which is written to w. 85 func InterfaceToFrontMatter(in interface{}, mark rune, w io.Writer) error { 86 if in == nil { 87 return errors.New("input was nil") 88 } 89 90 switch mark { 91 case rune(YAMLLead[0]): 92 _, err := w.Write([]byte(YAMLDelimUnix)) 93 if err != nil { 94 return err 95 } 96 97 err = InterfaceToConfig(in, mark, w) 98 if err != nil { 99 return err 100 } 101 102 _, err = w.Write([]byte(YAMLDelimUnix)) 103 return err 104 105 case rune(TOMLLead[0]): 106 _, err := w.Write([]byte(TOMLDelimUnix)) 107 if err != nil { 108 return err 109 } 110 111 err = InterfaceToConfig(in, mark, w) 112 113 if err != nil { 114 return err 115 } 116 117 _, err = w.Write([]byte("\n" + TOMLDelimUnix)) 118 return err 119 120 default: 121 return InterfaceToConfig(in, mark, w) 122 } 123 } 124 125 // FormatToLeadRune takes a given format kind and return the leading front 126 // matter delimiter. 127 func FormatToLeadRune(kind string) rune { 128 switch FormatSanitize(kind) { 129 case "yaml": 130 return rune([]byte(YAMLLead)[0]) 131 case "json": 132 return rune([]byte(JSONLead)[0]) 133 case "org": 134 return '#' 135 default: 136 return rune([]byte(TOMLLead)[0]) 137 } 138 } 139 140 // FormatSanitize returns the canonical format name for a given kind. 141 // 142 // TODO(bep) move to helpers 143 func FormatSanitize(kind string) string { 144 switch strings.ToLower(kind) { 145 case "yaml", "yml": 146 return "yaml" 147 case "toml", "tml": 148 return "toml" 149 case "json", "js": 150 return "json" 151 case "org": 152 return kind 153 default: 154 return "toml" 155 } 156 } 157 158 // DetectFrontMatter detects the type of frontmatter analysing its first character. 159 func DetectFrontMatter(mark rune) (f *FrontmatterType) { 160 switch mark { 161 case '-': 162 return &FrontmatterType{HandleYAMLMetaData, []byte(YAMLDelim), []byte(YAMLDelim), false} 163 case '+': 164 return &FrontmatterType{HandleTOMLMetaData, []byte(TOMLDelim), []byte(TOMLDelim), false} 165 case '{': 166 return &FrontmatterType{HandleJSONMetaData, []byte{'{'}, []byte{'}'}, true} 167 case '#': 168 return &FrontmatterType{HandleOrgMetaData, []byte("#+"), []byte("\n"), false} 169 default: 170 return nil 171 } 172 } 173 174 // HandleTOMLMetaData unmarshals TOML-encoded datum and returns a Go interface 175 // representing the encoded data structure. 176 func HandleTOMLMetaData(datum []byte) (map[string]interface{}, error) { 177 m := map[string]interface{}{} 178 datum = removeTOMLIdentifier(datum) 179 180 _, err := toml.Decode(string(datum), &m) 181 182 return m, err 183 184 } 185 186 // removeTOMLIdentifier removes, if necessary, beginning and ending TOML 187 // frontmatter delimiters from a byte slice. 188 func removeTOMLIdentifier(datum []byte) []byte { 189 ld := len(datum) 190 if ld < 8 { 191 return datum 192 } 193 194 b := bytes.TrimPrefix(datum, []byte(TOMLDelim)) 195 if ld-len(b) != 3 { 196 // No TOML prefix trimmed, so bail out 197 return datum 198 } 199 200 b = bytes.Trim(b, "\r\n") 201 return bytes.TrimSuffix(b, []byte(TOMLDelim)) 202 } 203 204 // HandleYAMLMetaData unmarshals YAML-encoded datum and returns a Go interface 205 // representing the encoded data structure. 206 func HandleYAMLMetaData(datum []byte) (map[string]interface{}, error) { 207 m := map[string]interface{}{} 208 err := yaml.Unmarshal(datum, &m) 209 210 // To support boolean keys, the `yaml` package unmarshals maps to 211 // map[interface{}]interface{}. Here we recurse through the result 212 // and change all maps to map[string]interface{} like we would've 213 // gotten from `json`. 214 if err == nil { 215 for k, v := range m { 216 if vv, changed := stringifyMapKeys(v); changed { 217 m[k] = vv 218 } 219 } 220 } 221 222 return m, err 223 } 224 225 // HandleYAMLData unmarshals YAML-encoded datum and returns a Go interface 226 // representing the encoded data structure. 227 func HandleYAMLData(datum []byte) (interface{}, error) { 228 var m interface{} 229 err := yaml.Unmarshal(datum, &m) 230 if err != nil { 231 return nil, err 232 } 233 234 // To support boolean keys, the `yaml` package unmarshals maps to 235 // map[interface{}]interface{}. Here we recurse through the result 236 // and change all maps to map[string]interface{} like we would've 237 // gotten from `json`. 238 if mm, changed := stringifyMapKeys(m); changed { 239 return mm, nil 240 } 241 242 return m, nil 243 } 244 245 // stringifyMapKeys recurses into in and changes all instances of 246 // map[interface{}]interface{} to map[string]interface{}. This is useful to 247 // work around the impedence mismatch between JSON and YAML unmarshaling that's 248 // described here: https://github.com/go-yaml/yaml/issues/139 249 // 250 // Inspired by https://github.com/stripe/stripe-mock, MIT licensed 251 func stringifyMapKeys(in interface{}) (interface{}, bool) { 252 switch in := in.(type) { 253 case []interface{}: 254 for i, v := range in { 255 if vv, replaced := stringifyMapKeys(v); replaced { 256 in[i] = vv 257 } 258 } 259 case map[interface{}]interface{}: 260 res := make(map[string]interface{}) 261 var ( 262 ok bool 263 err error 264 ) 265 for k, v := range in { 266 var ks string 267 268 if ks, ok = k.(string); !ok { 269 ks, err = cast.ToStringE(k) 270 if err != nil { 271 ks = fmt.Sprintf("%v", k) 272 } 273 // TODO(bep) added in Hugo 0.37, remove some time in the future. 274 helpers.DistinctFeedbackLog.Printf("WARNING: YAML data/frontmatter with keys of type %T is since Hugo 0.37 converted to strings", k) 275 } 276 if vv, replaced := stringifyMapKeys(v); replaced { 277 res[ks] = vv 278 } else { 279 res[ks] = v 280 } 281 } 282 return res, true 283 } 284 285 return nil, false 286 } 287 288 // HandleJSONMetaData unmarshals JSON-encoded datum and returns a Go interface 289 // representing the encoded data structure. 290 func HandleJSONMetaData(datum []byte) (map[string]interface{}, error) { 291 m := make(map[string]interface{}) 292 293 if datum == nil { 294 // Package json returns on error on nil input. 295 // Return an empty map to be consistent with our other supported 296 // formats. 297 return m, nil 298 } 299 300 err := json.Unmarshal(datum, &m) 301 return m, err 302 } 303 304 // HandleJSONData unmarshals JSON-encoded datum and returns a Go interface 305 // representing the encoded data structure. 306 func HandleJSONData(datum []byte) (interface{}, error) { 307 if datum == nil { 308 // Package json returns on error on nil input. 309 // Return an empty map to be consistent with our other supported 310 // formats. 311 return make(map[string]interface{}), nil 312 } 313 314 var f interface{} 315 err := json.Unmarshal(datum, &f) 316 return f, err 317 } 318 319 // HandleOrgMetaData unmarshals org-mode encoded datum and returns a Go 320 // interface representing the encoded data structure. 321 func HandleOrgMetaData(datum []byte) (map[string]interface{}, error) { 322 return goorgeous.OrgHeaders(datum) 323 }