parser: Refactor frontmatter parser and add tests
authorCameron Moore <moorereason@gmail.com>
Mon, 26 Dec 2016 21:23:20 +0000 (15:23 -0600)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 11 Mar 2017 16:52:25 +0000 (17:52 +0100)
Lots of cleanups here:

- Refactor InterfaceToConfig and InterfaceToFrontMatter to use io.Writer.
- Simplify InterfaceToFrontMatter by wrapping InterfaceToConfig.
- Export FrontmatterType since we return it in DetectFrontMatter.
- Refactor removeTOMLIdentifier to avoid blindly replacing "+++".
- Update HandleJSONMetaData to return an empty map on nil input.
- Updates vendored goorgeous package and test for org-mode frontmatter.
- Add tests and godoc comments.

Coverage for parser package increased from 45.2% to 85.2%.

commands/import_jekyll.go
commands/new.go
commands/undraft_test.go
hugolib/page.go
parser/frontmatter.go
parser/frontmatter_test.go
parser/page.go
parser/page_test.go [new file with mode: 0644]
vendor/vendor.json

index 151fffa8a26f8fe889134e3ce46c59c66a66e6f0..c33b68f2ed7d79f34e664de967e4a1269e02e6d0 100644 (file)
@@ -251,17 +251,13 @@ func createConfigFromJekyll(fs afero.Fs, inpath string, kind string, jekyllConfi
        }
        kind = parser.FormatSanitize(kind)
 
-       by, err := parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind))
+       var buf bytes.Buffer
+       err = parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind), &buf)
        if err != nil {
                return err
        }
 
-       err = helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), bytes.NewReader(by), fs)
-       if err != nil {
-               return
-       }
-
-       return nil
+       return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs)
 }
 
 func copyFile(source string, dest string) error {
index fbecb6854e5b63e5787bd6da1fd6d7dfdca78497..7bab13e979da817ca277c81d3ee7881624ff0413 100644 (file)
@@ -356,15 +356,11 @@ func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) {
        }
        kind = parser.FormatSanitize(kind)
 
-       by, err := parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind))
+       var buf bytes.Buffer
+       err = parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind), &buf)
        if err != nil {
                return err
        }
 
-       err = helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), bytes.NewReader(by), fs.Source)
-       if err != nil {
-               return
-       }
-
-       return nil
+       return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs.Source)
 }
index 45b785bb013da827be2fe31eee3b8bdd411d0a18..6ed172205dc2860a7517ebf423bf79f2c2257237 100644 (file)
@@ -46,17 +46,17 @@ func TestUndraftContent(t *testing.T) {
                {yamlDraftFM, ""},
        }
 
-       for _, test := range tests {
+       for i, test := range tests {
                r := bytes.NewReader([]byte(test.fm))
                p, _ := parser.ReadFrom(r)
                res, err := undraftContent(p)
                if test.expectedErr != "" {
                        if err == nil {
-                               t.Error("Expected error, got none")
+                               t.Error("[%d] Expected error, got none", i)
                                continue
                        }
                        if err.Error() != test.expectedErr {
-                               t.Errorf("Expected %q, got %q", test.expectedErr, err)
+                               t.Errorf("[%d] Expected %q, got %q", i, test.expectedErr, err)
                                continue
                        }
                } else {
@@ -64,19 +64,19 @@ func TestUndraftContent(t *testing.T) {
                        p, _ = parser.ReadFrom(r)
                        meta, err := p.Metadata()
                        if err != nil {
-                               t.Errorf("unexpected error %q", err)
+                               t.Errorf("[%d] unexpected error %q", i, err)
                                continue
                        }
                        for k, v := range meta.(map[string]interface{}) {
                                if k == "draft" {
                                        if v.(bool) {
-                                               t.Errorf("Expected %q to be \"false\", got \"true\"", k)
+                                               t.Errorf("[%d] Expected %q to be \"false\", got \"true\"", i, k)
                                                continue
                                        }
                                }
                                if k == "date" {
                                        if !strings.HasPrefix(v.(string), time.Now().Format("2006-01-02")) {
-                                               t.Errorf("Expected %v to start with %v", v.(string), time.Now().Format("2006-01-02"))
+                                               t.Errorf("[%d] Expected %v to start with %v", i, v.(string), time.Now().Format("2006-01-02"))
                                        }
                                }
                        }
index 02f889d93295255d4e8e7a15493e21f2f37e314d..5ee31c2a8592b2bf9b0f95927ccaf7d83e3bdbe4 100644 (file)
@@ -1424,15 +1424,20 @@ func (p *Page) SetSourceMetaData(in interface{}, mark rune) (err error) {
                }
        }()
 
-       var by []byte
+       buf := bp.GetBuffer()
+       defer bp.PutBuffer(buf)
 
-       by, err = parser.InterfaceToFrontMatter(in, mark)
+       err = parser.InterfaceToFrontMatter(in, mark, buf)
        if err != nil {
                return
        }
-       by = append(by, '\n')
 
-       p.Source.Frontmatter = by
+       _, err = buf.WriteRune('\n')
+       if err != nil {
+               return
+       }
+
+       p.Source.Frontmatter = buf.Bytes()
 
        return
 }
index e57a593ab4ec8f44beff9bb15dc5d04dcf924d69..797c6fcf01a4e48aafb136e605acd8fe3b13fbca 100644 (file)
@@ -17,6 +17,7 @@ import (
        "bytes"
        "encoding/json"
        "errors"
+       "io"
        "strings"
 
        "github.com/chaseadamsio/goorgeous"
@@ -25,113 +26,116 @@ import (
        "gopkg.in/yaml.v2"
 )
 
-type frontmatterType struct {
-       markstart, markend []byte
-       Parse              func([]byte) (interface{}, error)
-       includeMark        bool
+// FrontmatterType represents a type of frontmatter.
+type FrontmatterType struct {
+       // Parse decodes content into a Go interface.
+       Parse func([]byte) (interface{}, error)
+
+       markstart, markend []byte // starting and ending delimiters
+       includeMark        bool   // include start and end mark in output
 }
 
-func InterfaceToConfig(in interface{}, mark rune) ([]byte, error) {
+// InterfaceToConfig encodes a given input based upon the mark and writes to w.
+func InterfaceToConfig(in interface{}, mark rune, w io.Writer) error {
        if in == nil {
-               return []byte{}, errors.New("input was nil")
+               return errors.New("input was nil")
        }
 
-       b := new(bytes.Buffer)
-
        switch mark {
        case rune(YAMLLead[0]):
-               by, err := yaml.Marshal(in)
-               if err != nil {
-                       return nil, err
-               }
-               b.Write(by)
-               _, err = b.Write([]byte("..."))
+               b, err := yaml.Marshal(in)
                if err != nil {
-                       return nil, err
+                       return err
                }
-               return b.Bytes(), nil
+
+               _, err = w.Write(b)
+               return err
+
        case rune(TOMLLead[0]):
                tree := toml.TreeFromMap(in.(map[string]interface{}))
-               return []byte(tree.String()), nil
+               b := []byte(tree.String())
+
+               _, err := w.Write(b)
+               return err
+
        case rune(JSONLead[0]):
-               by, err := json.MarshalIndent(in, "", "   ")
+               b, err := json.MarshalIndent(in, "", "   ")
                if err != nil {
-                       return nil, err
+                       return err
                }
-               b.Write(by)
-               _, err = b.Write([]byte("\n"))
+
+               _, err = w.Write(b)
                if err != nil {
-                       return nil, err
+                       return err
                }
-               return b.Bytes(), nil
+
+               _, err = w.Write([]byte{'\n'})
+               return err
+
        default:
-               return nil, errors.New("Unsupported Format provided")
+               return errors.New("Unsupported Format provided")
        }
 }
 
-func InterfaceToFrontMatter(in interface{}, mark rune) ([]byte, error) {
+// InterfaceToFrontMatter encodes a given input into a frontmatter
+// representation based upon the mark with the appropriate front matter delimiters
+// surrounding the output, which is written to w.
+func InterfaceToFrontMatter(in interface{}, mark rune, w io.Writer) error {
        if in == nil {
-               return []byte{}, errors.New("input was nil")
+               return errors.New("input was nil")
        }
 
-       b := new(bytes.Buffer)
-
        switch mark {
        case rune(YAMLLead[0]):
-               _, err := b.Write([]byte(YAMLDelimUnix))
+               _, err := w.Write([]byte(YAMLDelimUnix))
                if err != nil {
-                       return nil, err
+                       return err
                }
-               by, err := yaml.Marshal(in)
-               if err != nil {
-                       return nil, err
-               }
-               b.Write(by)
-               _, err = b.Write([]byte(YAMLDelimUnix))
+
+               err = InterfaceToConfig(in, mark, w)
                if err != nil {
-                       return nil, err
+                       return err
                }
-               return b.Bytes(), nil
+
+               _, err = w.Write([]byte(YAMLDelimUnix))
+               return err
+
        case rune(TOMLLead[0]):
-               _, err := b.Write([]byte(TOMLDelimUnix))
+               _, err := w.Write([]byte(TOMLDelimUnix))
                if err != nil {
-                       return nil, err
+                       return err
                }
 
-               tree := toml.TreeFromMap(in.(map[string]interface{}))
-               b.Write([]byte(tree.String()))
-               _, err = b.Write([]byte("\n" + TOMLDelimUnix))
-               if err != nil {
-                       return nil, err
-               }
-               return b.Bytes(), nil
-       case rune(JSONLead[0]):
-               by, err := json.MarshalIndent(in, "", "   ")
-               if err != nil {
-                       return nil, err
-               }
-               b.Write(by)
-               _, err = b.Write([]byte("\n"))
+               err = InterfaceToConfig(in, mark, w)
                if err != nil {
-                       return nil, err
+                       return err
                }
-               return b.Bytes(), nil
+
+               _, err = w.Write([]byte("\n" + TOMLDelimUnix))
+               return err
+
        default:
-               return nil, errors.New("Unsupported Format provided")
+               return InterfaceToConfig(in, mark, w)
        }
 }
 
+// FormatToLeadRune takes a given format kind and return the leading front
+// matter delimiter.
 func FormatToLeadRune(kind string) rune {
        switch FormatSanitize(kind) {
        case "yaml":
                return rune([]byte(YAMLLead)[0])
        case "json":
                return rune([]byte(JSONLead)[0])
+       case "org":
+               return '#'
        default:
                return rune([]byte(TOMLLead)[0])
        }
 }
 
+// FormatSanitize returns the canonical format name for a given kind.
+//
 // TODO(bep) move to helpers
 func FormatSanitize(kind string) string {
        switch strings.ToLower(kind) {
@@ -141,27 +145,31 @@ func FormatSanitize(kind string) string {
                return "toml"
        case "json", "js":
                return "json"
+       case "org":
+               return kind
        default:
                return "toml"
        }
 }
 
 // DetectFrontMatter detects the type of frontmatter analysing its first character.
-func DetectFrontMatter(mark rune) (f *frontmatterType) {
+func DetectFrontMatter(mark rune) (f *FrontmatterType) {
        switch mark {
        case '-':
-               return &frontmatterType{[]byte(YAMLDelim), []byte(YAMLDelim), HandleYAMLMetaData, false}
+               return &FrontmatterType{HandleYAMLMetaData, []byte(YAMLDelim), []byte(YAMLDelim), false}
        case '+':
-               return &frontmatterType{[]byte(TOMLDelim), []byte(TOMLDelim), HandleTOMLMetaData, false}
+               return &FrontmatterType{HandleTOMLMetaData, []byte(TOMLDelim), []byte(TOMLDelim), false}
        case '{':
-               return &frontmatterType{[]byte{'{'}, []byte{'}'}, HandleJSONMetaData, true}
+               return &FrontmatterType{HandleJSONMetaData, []byte{'{'}, []byte{'}'}, true}
        case '#':
-               return &frontmatterType{[]byte("#+"), []byte("\n"), HandleOrgMetaData, false}
+               return &FrontmatterType{HandleOrgMetaData, []byte("#+"), []byte("\n"), false}
        default:
                return nil
        }
 }
 
+// HandleTOMLMetaData unmarshals TOML-encoded datum and returns a Go interface
+// representing the encoded data structure.
 func HandleTOMLMetaData(datum []byte) (interface{}, error) {
        m := map[string]interface{}{}
        datum = removeTOMLIdentifier(datum)
@@ -177,22 +185,49 @@ func HandleTOMLMetaData(datum []byte) (interface{}, error) {
        return m, nil
 }
 
+// removeTOMLIdentifier removes, if necessary, beginning and ending TOML
+// frontmatter delimiters from a byte slice.
 func removeTOMLIdentifier(datum []byte) []byte {
-       return bytes.Replace(datum, []byte(TOMLDelim), []byte(""), -1)
+       ld := len(datum)
+       if ld < 8 {
+               return datum
+       }
+
+       b := bytes.TrimPrefix(datum, []byte(TOMLDelim))
+       if ld-len(b) != 3 {
+               // No TOML prefix trimmed, so bail out
+               return datum
+       }
+
+       b = bytes.Trim(b, "\r\n")
+       return bytes.TrimSuffix(b, []byte(TOMLDelim))
 }
 
+// HandleYAMLMetaData unmarshals YAML-encoded datum and returns a Go interface
+// representing the encoded data structure.
 func HandleYAMLMetaData(datum []byte) (interface{}, error) {
        m := map[string]interface{}{}
        err := yaml.Unmarshal(datum, &m)
        return m, err
 }
 
+// HandleJSONMetaData unmarshals JSON-encoded datum and returns a Go interface
+// representing the encoded data structure.
 func HandleJSONMetaData(datum []byte) (interface{}, error) {
+       if datum == nil {
+               // Package json returns on error on nil input.
+               // Return an empty map to be consistent with our other supported
+               // formats.
+               return make(map[string]interface{}), nil
+       }
+
        var f interface{}
        err := json.Unmarshal(datum, &f)
        return f, err
 }
 
+// HandleOrgMetaData unmarshals org-mode encoded datum and returns a Go
+// interface representing the encoded data structure.
 func HandleOrgMetaData(datum []byte) (interface{}, error) {
        return goorgeous.OrgHeaders(datum)
 }
index 08191009455abd8927cc0dc0ac162ee192c18885..5aef3562f0b9f97b6a892ebf55e547424fde1cc0 100644 (file)
 package parser
 
 import (
+       "bytes"
+       "reflect"
        "testing"
 )
 
+func TestInterfaceToConfig(t *testing.T) {
+       cases := []struct {
+               input interface{}
+               mark  byte
+               want  []byte
+               isErr bool
+       }{
+               // TOML
+               {map[string]interface{}{}, TOMLLead[0], nil, false},
+               {
+                       map[string]interface{}{"title": "test 1"},
+                       TOMLLead[0],
+                       []byte("title = \"test 1\"\n"),
+                       false,
+               },
+
+               // YAML
+               {map[string]interface{}{}, YAMLLead[0], []byte("{}\n"), false},
+               {
+                       map[string]interface{}{"title": "test 1"},
+                       YAMLLead[0],
+                       []byte("title: test 1\n"),
+                       false,
+               },
+
+               // JSON
+               {map[string]interface{}{}, JSONLead[0], []byte("{}\n"), false},
+               {
+                       map[string]interface{}{"title": "test 1"},
+                       JSONLead[0],
+                       []byte("{\n   \"title\": \"test 1\"\n}\n"),
+                       false,
+               },
+
+               // Errors
+               {nil, TOMLLead[0], nil, true},
+               {map[string]interface{}{}, '$', nil, true},
+       }
+
+       for i, c := range cases {
+               var buf bytes.Buffer
+
+               err := InterfaceToConfig(c.input, rune(c.mark), &buf)
+               if err != nil {
+                       if c.isErr {
+                               continue
+                       }
+                       t.Fatalf("[%d] unexpected error value: %v", i, err)
+               }
+
+               if !reflect.DeepEqual(buf.Bytes(), c.want) {
+                       t.Errorf("[%d] not equal:\nwant %q,\n got %q", i, c.want, buf.Bytes())
+               }
+       }
+}
+
+func TestInterfaceToFrontMatter(t *testing.T) {
+       cases := []struct {
+               input interface{}
+               mark  rune
+               want  []byte
+               isErr bool
+       }{
+               // TOML
+               {map[string]interface{}{}, '+', []byte("+++\n\n+++\n"), false},
+               {
+                       map[string]interface{}{"title": "test 1"},
+                       '+',
+                       []byte("+++\ntitle = \"test 1\"\n\n+++\n"),
+                       false,
+               },
+
+               // YAML
+               {map[string]interface{}{}, '-', []byte("---\n{}\n---\n"), false}, //
+               {
+                       map[string]interface{}{"title": "test 1"},
+                       '-',
+                       []byte("---\ntitle: test 1\n---\n"),
+                       false,
+               },
+
+               // JSON
+               {map[string]interface{}{}, '{', []byte("{}\n"), false},
+               {
+                       map[string]interface{}{"title": "test 1"},
+                       '{',
+                       []byte("{\n   \"title\": \"test 1\"\n}\n"),
+                       false,
+               },
+
+               // Errors
+               {nil, '+', nil, true},
+               {map[string]interface{}{}, '$', nil, true},
+       }
+
+       for i, c := range cases {
+               var buf bytes.Buffer
+               err := InterfaceToFrontMatter(c.input, c.mark, &buf)
+               if err != nil {
+                       if c.isErr {
+                               continue
+                       }
+                       t.Fatalf("[%d] unexpected error value: %v", i, err)
+               }
+
+               if !reflect.DeepEqual(buf.Bytes(), c.want) {
+                       t.Errorf("[%d] not equal:\nwant %q,\n got %q", i, c.want, buf.Bytes())
+               }
+       }
+}
+
+func TestHandleTOMLMetaData(t *testing.T) {
+       cases := []struct {
+               input []byte
+               want  interface{}
+               isErr bool
+       }{
+               {nil, map[string]interface{}{}, false},
+               {[]byte("title = \"test 1\""), map[string]interface{}{"title": "test 1"}, false},
+               {[]byte("a = [1, 2, 3]"), map[string]interface{}{"a": []interface{}{int64(1), int64(2), int64(3)}}, false},
+               {[]byte("b = [\n[1, 2],\n[3, 4]\n]"), map[string]interface{}{"b": []interface{}{[]interface{}{int64(1), int64(2)}, []interface{}{int64(3), int64(4)}}}, false},
+               // errors
+               {[]byte("z = [\n[1, 2]\n[3, 4]\n]"), nil, true},
+       }
+
+       for i, c := range cases {
+               res, err := HandleTOMLMetaData(c.input)
+               if err != nil {
+                       if c.isErr {
+                               continue
+                       }
+                       t.Fatalf("[%d] unexpected error value: %v", i, err)
+               }
+
+               if !reflect.DeepEqual(res, c.want) {
+                       t.Errorf("[%d] not equal: given %q\nwant %#v,\n got %#v", i, c.input, c.want, res)
+               }
+       }
+}
+
+func TestHandleYAMLMetaData(t *testing.T) {
+       cases := []struct {
+               input []byte
+               want  interface{}
+               isErr bool
+       }{
+               {nil, map[string]interface{}{}, false},
+               {[]byte("title: test 1"), map[string]interface{}{"title": "test 1"}, false},
+               {[]byte("a: Easy!\nb:\n  c: 2\n  d: [3, 4]"), map[string]interface{}{"a": "Easy!", "b": map[interface{}]interface{}{"c": 2, "d": []interface{}{3, 4}}}, false},
+               // errors
+               {[]byte("z = not toml"), nil, true},
+       }
+
+       for i, c := range cases {
+               res, err := HandleYAMLMetaData(c.input)
+               if err != nil {
+                       if c.isErr {
+                               continue
+                       }
+                       t.Fatalf("[%d] unexpected error value: %v", i, err)
+               }
+
+               if !reflect.DeepEqual(res, c.want) {
+                       t.Errorf("[%d] not equal: given %q\nwant %#v,\n got %#v", i, c.input, c.want, res)
+               }
+       }
+}
+
+func TestHandleJSONMetaData(t *testing.T) {
+       cases := []struct {
+               input []byte
+               want  interface{}
+               isErr bool
+       }{
+               {nil, map[string]interface{}{}, false},
+               {[]byte("{\"title\": \"test 1\"}"), map[string]interface{}{"title": "test 1"}, false},
+               // errors
+               {[]byte("{noquotes}"), nil, true},
+       }
+
+       for i, c := range cases {
+               res, err := HandleJSONMetaData(c.input)
+               if err != nil {
+                       if c.isErr {
+                               continue
+                       }
+                       t.Fatalf("[%d] unexpected error value: %v", i, err)
+               }
+
+               if !reflect.DeepEqual(res, c.want) {
+                       t.Errorf("[%d] not equal: given %q\nwant %#v,\n got %#v", i, c.input, c.want, res)
+               }
+       }
+}
+
+func TestHandleOrgMetaData(t *testing.T) {
+       cases := []struct {
+               input []byte
+               want  interface{}
+               isErr bool
+       }{
+               {nil, map[string]interface{}{}, false},
+               {[]byte("#+title: test 1\n"), map[string]interface{}{"title": "test 1"}, false},
+       }
+
+       for i, c := range cases {
+               res, err := HandleOrgMetaData(c.input)
+               if err != nil {
+                       if c.isErr {
+                               continue
+                       }
+                       t.Fatalf("[%d] unexpected error value: %v", i, err)
+               }
+
+               if !reflect.DeepEqual(res, c.want) {
+                       t.Errorf("[%d] not equal: given %q\nwant %#v,\n got %#v", i, c.input, c.want, res)
+               }
+       }
+}
+
 func TestFormatToLeadRune(t *testing.T) {
        for i, this := range []struct {
                kind   string
@@ -25,8 +247,10 @@ func TestFormatToLeadRune(t *testing.T) {
                {"yaml", '-'},
                {"yml", '-'},
                {"toml", '+'},
+               {"tml", '+'},
                {"json", '{'},
                {"js", '{'},
+               {"org", '#'},
                {"unknown", '+'},
        } {
                result := FormatToLeadRune(this.kind)
@@ -36,3 +260,60 @@ func TestFormatToLeadRune(t *testing.T) {
                }
        }
 }
+
+func TestDetectFrontMatter(t *testing.T) {
+       cases := []struct {
+               mark rune
+               want *FrontmatterType
+       }{
+               // funcs are uncomparable, so we ignore FrontmatterType.Parse in these tests
+               {'-', &FrontmatterType{nil, []byte(YAMLDelim), []byte(YAMLDelim), false}},
+               {'+', &FrontmatterType{nil, []byte(TOMLDelim), []byte(TOMLDelim), false}},
+               {'{', &FrontmatterType{nil, []byte("{"), []byte("}"), true}},
+               {'#', &FrontmatterType{nil, []byte("#+"), []byte("\n"), false}},
+               {'$', nil},
+       }
+
+       for _, c := range cases {
+               res := DetectFrontMatter(c.mark)
+               if res == nil {
+                       if c.want == nil {
+                               continue
+                       }
+
+                       t.Fatalf("want %v, got %v", *c.want, res)
+               }
+
+               if !reflect.DeepEqual(res.markstart, c.want.markstart) {
+                       t.Errorf("markstart mismatch: want %v, got %v", c.want.markstart, res.markstart)
+               }
+               if !reflect.DeepEqual(res.markend, c.want.markend) {
+                       t.Errorf("markend mismatch: want %v, got %v", c.want.markend, res.markend)
+               }
+               if !reflect.DeepEqual(res.includeMark, c.want.includeMark) {
+                       t.Errorf("includeMark mismatch: want %v, got %v", c.want.includeMark, res.includeMark)
+               }
+       }
+}
+
+func TestRemoveTOMLIdentifier(t *testing.T) {
+       cases := []struct {
+               input string
+               want  string
+       }{
+               {"a = 1", "a = 1"},
+               {"a = 1\r\n", "a = 1\r\n"},
+               {"+++\r\na = 1\r\n+++\r\n", "a = 1\r\n"},
+               {"+++\na = 1\n+++\n", "a = 1\n"},
+               {"+++\nb = \"+++ oops +++\"\n+++\n", "b = \"+++ oops +++\"\n"},
+               {"+++\nc = \"\"\"+++\noops\n+++\n\"\"\"\"\n+++\n", "c = \"\"\"+++\noops\n+++\n\"\"\"\"\n"},
+               {"+++\nd = 1\n+++", "d = 1\n"},
+       }
+
+       for i, c := range cases {
+               res := removeTOMLIdentifier([]byte(c.input))
+               if string(res) != c.want {
+                       t.Errorf("[%d] given %q\nwant: %q\n got: %q", i, c.input, c.want, res)
+               }
+       }
+}
index 3347380d7f37ca624aeabc943f73c8b1e3e812a6..a0679289cd926f5082a660a7dc84e10bf1d26305 100644 (file)
@@ -64,30 +64,42 @@ var (
 
 // Page represents a parsed content page.
 type Page interface {
+       // FrontMatter contains the raw frontmatter with relevant delimiters.
        FrontMatter() []byte
+
+       // Content contains the raw page content.
        Content() []byte
+
+       // IsRenderable denotes that the page should be rendered.
        IsRenderable() bool
+
+       // Metadata returns the unmarshalled frontmatter data.
        Metadata() (interface{}, error)
 }
 
+// page implements the Page interface.
 type page struct {
        render      bool
        frontmatter []byte
        content     []byte
 }
 
+// Content returns the raw page content.
 func (p *page) Content() []byte {
        return p.content
 }
 
+// FrontMatter contains the raw frontmatter with relevant delimiters.
 func (p *page) FrontMatter() []byte {
        return p.frontmatter
 }
 
+// IsRenderable denotes that the page should be rendered.
 func (p *page) IsRenderable() bool {
        return p.render
 }
 
+// Metadata returns the unmarshalled frontmatter data.
 func (p *page) Metadata() (meta interface{}, err error) {
        frontmatter := p.FrontMatter()
 
@@ -151,6 +163,7 @@ func ReadFrom(r io.Reader) (p Page, err error) {
        return newp, nil
 }
 
+// chompBOM scans any leading Unicode Byte Order Markers from r.
 func chompBOM(r io.RuneScanner) (err error) {
        for {
                c, _, err := r.ReadRune()
@@ -164,6 +177,7 @@ func chompBOM(r io.RuneScanner) (err error) {
        }
 }
 
+// chompWhitespace scans any leading Unicode whitespace from r.
 func chompWhitespace(r io.RuneScanner) (err error) {
        for {
                c, _, err := r.ReadRune()
@@ -177,6 +191,9 @@ func chompWhitespace(r io.RuneScanner) (err error) {
        }
 }
 
+// chompFrontmatterStartComment checks r for a leading HTML comment.  If a
+// comment is found, it is read from r and then whitespace is trimmed from the
+// beginning of r.
 func chompFrontmatterStartComment(r *bufio.Reader) (err error) {
        candidate, err := r.Peek(32)
        if err != nil {
@@ -206,6 +223,7 @@ func chompFrontmatterStartComment(r *bufio.Reader) (err error) {
        return nil
 }
 
+// chompFrontmatterEndComment checks r for a trailing HTML comment.
 func chompFrontmatterEndComment(r *bufio.Reader) (err error) {
        candidate, err := r.Peek(32)
        if err != nil {
diff --git a/parser/page_test.go b/parser/page_test.go
new file mode 100644 (file)
index 0000000..07d7660
--- /dev/null
@@ -0,0 +1,130 @@
+package parser
+
+import (
+       "fmt"
+       "strings"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+func TestPage(t *testing.T) {
+       cases := []struct {
+               raw string
+
+               content     string
+               frontmatter string
+               renderable  bool
+               metadata    map[string]interface{}
+       }{
+               {
+                       testPageLeader + jsonPageFrontMatter + "\n" + testPageTrailer + jsonPageContent,
+                       jsonPageContent,
+                       jsonPageFrontMatter,
+                       true,
+                       map[string]interface{}{
+                               "title": "JSON Test 1",
+                               "social": []interface{}{
+                                       []interface{}{"a", "#"},
+                                       []interface{}{"b", "#"},
+                               },
+                       },
+               },
+               {
+                       testPageLeader + tomlPageFrontMatter + testPageTrailer + tomlPageContent,
+                       tomlPageContent,
+                       tomlPageFrontMatter,
+                       true,
+                       map[string]interface{}{
+                               "title": "TOML Test 1",
+                               "social": []interface{}{
+                                       []interface{}{"a", "#"},
+                                       []interface{}{"b", "#"},
+                               },
+                       },
+               },
+               {
+                       testPageLeader + yamlPageFrontMatter + testPageTrailer + yamlPageContent,
+                       yamlPageContent,
+                       yamlPageFrontMatter,
+                       true,
+                       map[string]interface{}{
+                               "title": "YAML Test 1",
+                               "social": []interface{}{
+                                       []interface{}{"a", "#"},
+                                       []interface{}{"b", "#"},
+                               },
+                       },
+               },
+               {
+                       testPageLeader + orgPageFrontMatter + orgPageContent,
+                       orgPageContent,
+                       orgPageFrontMatter,
+                       true,
+                       map[string]interface{}{
+                               "TITLE":      "Org Test 1",
+                               "categories": []string{"a", "b"},
+                       },
+               },
+       }
+
+       for i, c := range cases {
+               p := pageMust(ReadFrom(strings.NewReader(c.raw)))
+               meta, err := p.Metadata()
+
+               mesg := fmt.Sprintf("[%d]", i)
+
+               require.Nil(t, err, mesg)
+               assert.Equal(t, c.content, string(p.Content()), mesg+" content")
+               assert.Equal(t, c.frontmatter, string(p.FrontMatter()), mesg+" frontmatter")
+               assert.Equal(t, c.renderable, p.IsRenderable(), mesg+" renderable")
+               assert.Equal(t, c.metadata, meta, mesg+" metadata")
+       }
+}
+
+var (
+       testWhitespace  = "\t\t\n\n"
+       testPageLeader  = "\ufeff" + testWhitespace + "<!--[metadata]>\n"
+       testPageTrailer = "\n<![end-metadata]-->\n"
+
+       jsonPageContent     = "# JSON Test\n"
+       jsonPageFrontMatter = `{
+       "title": "JSON Test 1",
+       "social": [
+               ["a", "#"],
+               ["b", "#"]
+       ]
+}`
+
+       tomlPageContent     = "# TOML Test\n"
+       tomlPageFrontMatter = `+++
+title = "TOML Test 1"
+social = [
+       ["a", "#"],
+       ["b", "#"],
+]
++++
+`
+
+       yamlPageContent     = "# YAML Test\n"
+       yamlPageFrontMatter = `---
+title: YAML Test 1
+social:
+  - - "a"
+    - "#"
+  - - "b"
+    - "#"
+---
+`
+
+       orgPageContent     = "* Org Test\n"
+       orgPageFrontMatter = `#+TITLE: Org Test 1
+#+categories: a b
+`
+
+       pageHTMLComment = `<!--
+       This is a sample comment.
+-->
+`
+)
index 738eddfe6f801bc838e205e5b3eebe2799af7f36..7891d3ef40c4edd2bb9838963c82640d9e514259 100644 (file)
                        "revisionTime": "2016-04-08T19:03:23Z"
                },
                {
-                       "checksumSHA1": "RxIwAgjIuBpwde5BCZRLLK7VRG8=",
+                       "checksumSHA1": "tOtpDG/zYOvYRQeSHcg8IhZnRHQ=",
                        "path": "github.com/chaseadamsio/goorgeous",
-                       "revision": "72a06e1b07db57f3931f5a9c00f3f04e636ad0a8",
-                       "revisionTime": "2017-02-17T13:03:04Z"
+                       "revision": "054aba677f27bd60872cfe68f8145dc57bdf4746",
+                       "revisionTime": "2017-02-22T05:25:03Z"
                },
                {
                        "checksumSHA1": "ntacCkWfMT63DaehXLG5FeXWyNM=",