shithub: hugo

Download patch

ref: f039e3be9e4a11808508c8cd3043b340deea040f
parent: ddc8cc0082965143a650052a9aa538bac9133481
author: Cameron Moore <[email protected]>
date: Mon Dec 26 10:23:20 EST 2016

parser: Refactor frontmatter parser and add tests

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%.

--- a/commands/import_jekyll.go
+++ b/commands/import_jekyll.go
@@ -251,17 +251,13 @@
 	}
 	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 {
--- a/commands/new.go
+++ b/commands/new.go
@@ -356,15 +356,11 @@
 	}
 	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)
 }
--- a/commands/undraft_test.go
+++ b/commands/undraft_test.go
@@ -46,17 +46,17 @@
 		{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 @@
 			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"))
 					}
 				}
 			}
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -1424,15 +1424,20 @@
 		}
 	}()
 
-	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
 }
--- a/parser/frontmatter.go
+++ b/parser/frontmatter.go
@@ -17,6 +17,7 @@
 	"bytes"
 	"encoding/json"
 	"errors"
+	"io"
 	"strings"
 
 	"github.com/chaseadamsio/goorgeous"
@@ -25,102 +26,101 @@
 	"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)
+		b, err := yaml.Marshal(in)
 		if err != nil {
-			return nil, err
+			return err
 		}
-		b.Write(by)
-		_, err = b.Write([]byte("..."))
-		if err != nil {
-			return nil, 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)
+
+		err = InterfaceToConfig(in, mark, w)
 		if err != nil {
-			return nil, err
+			return err
 		}
-		b.Write(by)
-		_, err = b.Write([]byte(YAMLDelimUnix))
-		if err != nil {
-			return nil, 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))
+		err = InterfaceToConfig(in, mark, w)
 		if err != nil {
-			return nil, err
+			return 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"))
-		if err != nil {
-			return nil, 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":
@@ -127,11 +127,15 @@
 		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,6 +145,8 @@
 		return "toml"
 	case "json", "js":
 		return "json"
+	case "org":
+		return kind
 	default:
 		return "toml"
 	}
@@ -147,21 +153,23 @@
 }
 
 // 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,10 +185,26 @@
 	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)
@@ -187,12 +211,23 @@
 	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)
 }
--- a/parser/frontmatter_test.go
+++ b/parser/frontmatter_test.go
@@ -14,9 +14,231 @@
 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 @@
 		{"yaml", '-'},
 		{"yml", '-'},
 		{"toml", '+'},
+		{"tml", '+'},
 		{"json", '{'},
 		{"js", '{'},
+		{"org", '#'},
 		{"unknown", '+'},
 	} {
 		result := FormatToLeadRune(this.kind)
@@ -33,6 +257,63 @@
 
 		if result != this.expect {
 			t.Errorf("[%d] got %q but expected %q", i, result, this.expect)
+		}
+	}
+}
+
+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)
 		}
 	}
 }
--- a/parser/page.go
+++ b/parser/page.go
@@ -64,12 +64,20 @@
 
 // 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
@@ -76,18 +84,22 @@
 	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 @@
 	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 @@
 	}
 }
 
+// chompWhitespace scans any leading Unicode whitespace from r.
 func chompWhitespace(r io.RuneScanner) (err error) {
 	for {
 		c, _, err := r.ReadRune()
@@ -177,6 +191,9 @@
 	}
 }
 
+// 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 @@
 	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 {
--- /dev/null
+++ b/parser/page_test.go
@@ -1,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.
+-->
+`
+)
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -33,10 +33,10 @@
 			"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=",