package frontmatter import ( "bytes" "encoding/json" "errors" "log" "reflect" "sort" "strconv" "strings" "gopkg.in/yaml.v2" "github.com/BurntSushi/toml" "github.com/spf13/cast" ) const ( mainName = "#MAIN#" objectType = "object" arrayType = "array" ) var mainTitle = "" // Pretty creates a new FrontMatter object func Pretty(content []byte) (*Content, string, error) { data, err := Unmarshal(content) if err != nil { return &Content{}, "", err } kind := reflect.ValueOf(data).Kind() if kind == reflect.Invalid { return &Content{}, "", nil } object := new(Block) object.Type = objectType object.Name = mainName if kind == reflect.Map { object.Type = objectType } else if kind == reflect.Slice || kind == reflect.Array { object.Type = arrayType } return rawToPretty(data, object), mainTitle, nil } // Unmarshal returns the data of the frontmatter func Unmarshal(content []byte) (interface{}, error) { mark := rune(content[0]) var data interface{} switch mark { case '-': // If it's YAML if err := yaml.Unmarshal(content, &data); err != nil { return nil, err } case '+': // If it's TOML content = bytes.Replace(content, []byte("+"), []byte(""), -1) if _, err := toml.Decode(string(content), &data); err != nil { return nil, err } case '{', '[': // If it's JSON if err := json.Unmarshal(content, &data); err != nil { return nil, err } default: return nil, errors.New("Invalid frontmatter type") } return data, nil } // Marshal encodes the interface in a specific format func Marshal(data interface{}, mark rune) ([]byte, error) { b := new(bytes.Buffer) switch mark { case '+': enc := toml.NewEncoder(b) err := enc.Encode(data) if err != nil { return nil, err } return b.Bytes(), nil case '{': by, err := json.MarshalIndent(data, "", " ") if err != nil { return nil, err } b.Write(by) _, err = b.Write([]byte("\n")) if err != nil { return nil, err } return b.Bytes(), nil case '-': by, err := yaml.Marshal(data) if err != nil { return nil, err } b.Write(by) _, err = b.Write([]byte("...")) if err != nil { return nil, err } return b.Bytes(), nil default: return nil, errors.New("Unsupported Format provided") } } // Content is the block content type Content struct { Other interface{} Fields []*Block Arrays []*Block Objects []*Block } // Block is a block type Block struct { Name string Title string Type string HTMLType string Content *Content Parent *Block } func rawToPretty(config interface{}, parent *Block) *Content { objects := []*Block{} arrays := []*Block{} fields := []*Block{} cnf := map[string]interface{}{} kind := reflect.TypeOf(config) switch kind { case reflect.TypeOf(map[interface{}]interface{}{}): for key, value := range config.(map[interface{}]interface{}) { cnf[key.(string)] = value } case reflect.TypeOf([]map[string]interface{}{}): for index, value := range config.([]map[string]interface{}) { cnf[strconv.Itoa(index)] = value } case reflect.TypeOf([]map[interface{}]interface{}{}): for index, value := range config.([]map[interface{}]interface{}) { cnf[strconv.Itoa(index)] = value } case reflect.TypeOf([]interface{}{}): for index, value := range config.([]interface{}) { cnf[strconv.Itoa(index)] = value } default: cnf = config.(map[string]interface{}) } for name, element := range cnf { if isMap(element) { objects = append(objects, handleObjects(element, parent, name)) } else if isSlice(element) { arrays = append(arrays, handleArrays(element, parent, name)) } else { if name == "title" && parent.Name == mainName { mainTitle = element.(string) } fields = append(fields, handleFlatValues(element, parent, name)) } } sort.Sort(sortByTitle(fields)) sort.Sort(sortByTitle(arrays)) sort.Sort(sortByTitle(objects)) return &Content{ Fields: fields, Arrays: arrays, Objects: objects, } } type sortByTitle []*Block func (f sortByTitle) Len() int { return len(f) } func (f sortByTitle) Swap(i, j int) { f[i], f[j] = f[j], f[i] } func (f sortByTitle) Less(i, j int) bool { return strings.ToLower(f[i].Name) < strings.ToLower(f[j].Name) } func handleObjects(content interface{}, parent *Block, name string) *Block { c := new(Block) c.Parent = parent c.Type = objectType c.Title = name if parent.Name == mainName { c.Name = c.Title } else if parent.Type == arrayType { c.Name = parent.Name + "[" + name + "]" } else { c.Name = parent.Name + "." + c.Title } c.Content = rawToPretty(content, c) return c } func handleArrays(content interface{}, parent *Block, name string) *Block { c := new(Block) c.Parent = parent c.Type = arrayType c.Title = name if parent.Name == mainName { c.Name = name } else { c.Name = parent.Name + "." + name } c.Content = rawToPretty(content, c) return c } func handleFlatValues(content interface{}, parent *Block, name string) *Block { c := new(Block) c.Parent = parent switch content.(type) { case bool: c.Type = "boolean" case int, float32, float64: c.Type = "number" default: c.Type = "string" } c.Content = &Content{Other: content} switch strings.ToLower(name) { case "description": c.HTMLType = "textarea" case "date", "publishdate": c.HTMLType = "datetime" c.Content = &Content{Other: cast.ToTime(content)} default: c.HTMLType = "text" } if parent.Type == arrayType { c.Name = parent.Name + "[]" c.Title = content.(string) } else if parent.Type == objectType { c.Title = name c.Name = parent.Name + "." + name if parent.Name == mainName { c.Name = name } } else { log.Panic("Parent type not allowed in handleFlatValues.") } return c }