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  }