ref: 085ce15f7c041ba71772a0cd6eed4f06b6b0ca34
parent: 274d324c8bb818a76ffc5c35afcc33f4cf9eb5c3
author: Noah Campbell <noahcampbell@gmail.com>
date: Mon Aug 5 03:53:58 EDT 2013
Adding ability to read from io.Reader This allows for testing without relying on the file system. Parsing algorithm to not read the entire file into memory.
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -14,12 +14,15 @@
package hugolib
import (
+ "bufio"
"bytes"
"encoding/json"
+ "errors"
"fmt"
"github.com/BurntSushi/toml"
"github.com/theplant/blackfriday"
"html/template"
+ "io"
"io/ioutil"
"launchpad.net/goyaml"
"os"
@@ -28,6 +31,7 @@
"sort"
"strings"
"time"
+ "unicode"
)
var _ = filepath.Base("")@@ -126,6 +130,22 @@
return strings.ToLower(page.Type()) + "/" + layout + ".html"
}
+func ReadFrom(buf io.Reader, name string) (page *Page, err error) {+ if len(name) == 0 {+ return nil, errors.New("Zero length page name")+ }
+
+ p := initializePage(name)
+
+ if err = p.parse(buf); err != nil {+ return
+ }
+
+ p.analyzePage()
+
+ return &p, nil
+}
+
// TODO should return errors as well
// TODO new page should return just a page
// TODO initalize separately... load from reader (file, or []byte)
@@ -133,7 +153,6 @@
p := initializePage(filename)
if err := p.buildPageFromFile(); err != nil {fmt.Println(err)
- os.Exit(1)
}
p.analyzePage()
@@ -146,49 +165,6 @@
p.FuzzyWordCount = int((p.WordCount+100)/100) * 100
}
-// TODO //rewrite to use byte methods instead
-func (page *Page) parseYamlMetaData(data []byte) ([]string, error) {- var err error
-
- datum, lines := splitPageContent(data, "---", "---")
- d, err := page.handleYamlMetaData([]byte(strings.Join(datum, "\n")))
-
- if err != nil {- return lines, err
- }
-
- err = page.handleMetaData(d)
- return lines, err
-}
-
-func (page *Page) parseTomlMetaData(data []byte) ([]string, error) {- var err error
-
- datum, lines := splitPageContent(data, "+++", "+++")
- d, err := page.handleTomlMetaData([]byte(strings.Join(datum, "\n")))
-
- if err != nil {- return lines, err
- }
-
- err = page.handleMetaData(d)
- return lines, err
-}
-
-func (page *Page) parseJsonMetaData(data []byte) ([]string, error) {- var err error
-
- datum, lines := splitPageContent(data, "{", "}")- d, err := page.handleJsonMetaData([]byte(strings.Join(datum, "\n")))
-
- if err != nil {- return lines, err
- }
-
- err = page.handleMetaData(d)
- return lines, err
-}
-
func splitPageContent(data []byte, start string, end string) ([]string, []string) {lines := strings.Split(string(data), "\n")
datum := lines[0:]
@@ -211,18 +187,6 @@
break
}
}
- } else { // Start token & end token are the same- for i, line := range lines {- if found == 1 && strings.HasPrefix(line, end) {- datum = lines[1:i]
- lines = lines[i+1:]
- break
- }
-
- if found == 0 && strings.HasPrefix(line, start) {- found = 1
- }
- }
}
return datum, lines
}
@@ -272,7 +236,7 @@
return f, nil
}
-func (page *Page) handleMetaData(f interface{}) error {+func (page *Page) update(f interface{}) error { m := f.(map[string]interface{}) for k, v := range m {@@ -304,7 +268,6 @@
page.Status = interfaceToString(v)
default:
// If not one of the explicit values, store in Params
- //fmt.Println(strings.ToLower(k))
switch vv := v.(type) {case string: // handle string values
page.Params[strings.ToLower(k)] = vv
@@ -340,25 +303,106 @@
return nil
}
-func (page *Page) Err(message string) {- fmt.Println(page.FileName + " : " + message)
+// TODO return error on last line instead of nil
+func (page *Page) parseFrontMatter(data *bufio.Reader) (err error) {+
+ if err = checkEmpty(data); err != nil {+ return err
+ }
+
+ var mark rune
+ if mark, err = chompWhitespace(data); err != nil {+ return err
+ }
+
+ f := page.detectFrontMatter(mark)
+ if f == nil {+ return errors.New("unable to match beginning front matter delimiter")+ }
+
+ if found, err := beginFrontMatter(data, f); err != nil || !found {+ return errors.New("unable to match beginning front matter delimiter")+ }
+
+ var frontmatter = new(bytes.Buffer)
+ for {+ line, _, err := data.ReadLine()
+ if err != nil {+ if err == io.EOF {+ return errors.New("unable to match ending front matter delimiter")+ }
+ return err
+ }
+ if bytes.Equal(line, f.markend) {+ break
+ }
+ frontmatter.Write(line)
+ frontmatter.Write([]byte{'\n'})+ }
+
+ metadata, err := f.parse(frontmatter.Bytes())
+ if err != nil {+ return err
+ }
+
+ if err = page.update(metadata); err != nil {+ return err
+ }
+
+ return
}
-// TODO return error on last line instead of nil
-func (page *Page) parseFileHeading(data []byte) ([]string, error) {- if len(data) == 0 {- page.Err("Empty File, skipping")- } else {- switch data[0] {- case '{':- return page.parseJsonMetaData(data)
- case '-':
- return page.parseYamlMetaData(data)
- case '+':
- return page.parseTomlMetaData(data)
+func checkEmpty(data *bufio.Reader) (err error) {+ if _, _, err = data.ReadRune(); err != nil {+ return errors.New("unable to locate front matter")+ }
+ if err = data.UnreadRune(); err != nil {+ return errors.New("unable to unread first charactor in page buffer.")+ }
+ return
+}
+
+type frontmatterType struct {+ markstart, markend []byte
+ parse func([]byte) (interface{}, error)+}
+
+func (page *Page) detectFrontMatter(mark rune) (f *frontmatterType) {+ switch mark {+ case '-':
+ return &frontmatterType{[]byte{'-', '-', '-'}, []byte{'-', '-', '-'}, page.handleYamlMetaData}+ case '+':
+ return &frontmatterType{[]byte{'+', '+', '+'}, []byte{'+', '+', '+'}, page.handleTomlMetaData}+ case '{':+ return &frontmatterType{[]byte{'{'}, []byte{'}'}, page.handleJsonMetaData}+ default:
+ return nil
+ }
+}
+
+func beginFrontMatter(data *bufio.Reader, f *frontmatterType) (bool, error) {+ peek := make([]byte, 3)
+ _, err := data.Read(peek)
+ if err != nil {+ return false, err
+ }
+ return bytes.Equal(peek, f.markstart), nil
+}
+
+func chompWhitespace(data *bufio.Reader) (r rune, err error) {+ for {+ r, _, err = data.ReadRune()
+ if err != nil {+ return
}
+ if unicode.IsSpace(r) {+ continue
+ }
+ if err := data.UnreadRune(); err != nil {+ return r, errors.New("unable to unread first charactor in front matter.")+ }
+ return r, nil
}
- return nil, nil
}
func (p *Page) Render(layout ...string) template.HTML {@@ -378,19 +422,26 @@
return buffer
}
-func (page *Page) readFile() []byte {- var data, err = ioutil.ReadFile(page.FileName)
+func (page *Page) readFile() (data []byte, err error) {+ data, err = ioutil.ReadFile(page.FileName)
if err != nil {- PrintErr("Error Reading: " + page.FileName)- return nil
+ return nil, err
}
- return data
+ return data, nil
}
func (page *Page) buildPageFromFile() error {- data := page.readFile()
+ f, err := os.Open(page.FileName)
+ if err != nil {+ return err
+ }
+ return page.parse(bufio.NewReader(f))
+}
- content, err := page.parseFileHeading(data)
+func (page *Page) parse(reader io.Reader) error {+ data := bufio.NewReader(reader)
+
+ err := page.parseFrontMatter(data)
if err != nil {return err
}
@@ -397,27 +448,24 @@
switch page.Markup {case "md":
- page.convertMarkdown(content)
+ page.convertMarkdown(data)
case "rst":
- page.convertRestructuredText(content)
+ page.convertRestructuredText(data)
}
return nil
}
-func (page *Page) convertMarkdown(lines []string) {-
- page.RawMarkdown = strings.Join(lines, "\n")
- content := string(blackfriday.MarkdownCommon([]byte(page.RawMarkdown)))
+func (page *Page) convertMarkdown(lines io.Reader) {+ b := new(bytes.Buffer)
+ b.ReadFrom(lines)
+ content := string(blackfriday.MarkdownCommon(b.Bytes()))
page.Content = template.HTML(content)
page.Summary = template.HTML(TruncateWordsToWholeSentence(StripHTML(StripShortcodes(content)), summaryLength))
}
-func (page *Page) convertRestructuredText(lines []string) {-
- page.RawMarkdown = strings.Join(lines, "\n")
-
+func (page *Page) convertRestructuredText(lines io.Reader) { cmd := exec.Command("rst2html.py")- cmd.Stdin = strings.NewReader(page.RawMarkdown)
+ cmd.Stdin = lines
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {--- /dev/null
+++ b/hugolib/page_test.go
@@ -1,0 +1,124 @@
+package hugolib
+
+import (
+ "html/template"
+ "io"
+ "strings"
+ "testing"
+)
+
+var EMPTY_PAGE = ""
+
+var SIMPLE_PAGE = `---
+title: Simple
+---
+Simple Page
+`
+
+var INVALID_FRONT_MATTER_MISSING = `This is a test`
+
+var INVALID_FRONT_MATTER_SHORT_DELIM = `
+--
+title: Short delim start
+---
+Short Delim
+`
+
+var INVALID_FRONT_MATTER_SHORT_DELIM_ENDING = `
+---
+title: Short delim ending
+--
+Short Delim
+`
+
+var INVALID_FRONT_MATTER_LEADING_WS = `
+
+ ---
+title: Leading WS
+---
+Leading
+`
+
+func checkError(t *testing.T, err error, expected string) {+ if err == nil {+ t.Fatalf("err is nil")+ }
+ if err.Error() != expected {+ t.Errorf("err.Error() returned: '%s'. Expected: '%s'", err.Error(), expected)+ }
+}
+
+func TestDegenerateEmptyPageZeroLengthName(t *testing.T) {+ _, err := ReadFrom(strings.NewReader(EMPTY_PAGE), "")
+ if err == nil {+ t.Fatalf("A zero length page name must return an error")+ }
+
+ checkError(t, err, "Zero length page name")
+}
+
+func TestDegenerateEmptyPage(t *testing.T) {+ _, err := ReadFrom(strings.NewReader(EMPTY_PAGE), "test")
+ if err == nil {+ t.Fatalf("Expected ReadFrom to return an error when an empty buffer is passed.")+ }
+
+ checkError(t, err, "unable to locate front matter")
+}
+
+func checkPageTitle(t *testing.T, page *Page, title string) {+ if page.Title != title {+ t.Fatalf("Page title is: %s. Expected %s", page.Title, title)+ }
+}
+
+func checkPageContent(t *testing.T, page *Page, content string) {+ if page.Content != template.HTML(content) {+ t.Fatalf("Page content is: %s. Expected %s", page.Content, content)+ }
+}
+
+func checkPageType(t *testing.T, page *Page, pageType string) {+ if page.Type() != pageType {+ t.Fatalf("Page type is: %s. Expected: %s", page.Type(), pageType)+ }
+}
+
+func checkPageLayout(t *testing.T, page *Page, layout string) {+ if page.Layout() != layout {+ t.Fatalf("Page layout is: %s. Expected: %s", page.Layout(), layout)+ }
+}
+
+func TestCreateNewPage(t *testing.T) {+ p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE), "simple")
+ if err != nil {+ t.Fatalf("Unable to create a page with frontmatter and body content: %s", err)+ }
+ checkPageTitle(t, p, "Simple")
+ checkPageContent(t, p, "<p>Simple Page</p>\n")
+ checkPageType(t, p, "page")
+ checkPageLayout(t, p, "page/single.html")
+}
+
+func TestDegenerateInvalidFrontMatterShortDelim(t *testing.T) {+ var tests = []struct {+ r io.Reader
+ err string
+ }{+ {strings.NewReader(INVALID_FRONT_MATTER_SHORT_DELIM), "unable to match beginning front matter delimiter"},+ {strings.NewReader(INVALID_FRONT_MATTER_SHORT_DELIM_ENDING), "unable to match ending front matter delimiter"},+ {strings.NewReader(INVALID_FRONT_MATTER_MISSING), "unable to match beginning front matter delimiter"},+ }
+ for _, test := range tests {+ _, err := ReadFrom(test.r, "invalid/front/matter/short/delim")
+ checkError(t, err, test.err)
+ }
+}
+
+func TestDegenerateInvalidFrontMatterLeadingWhitespace(t *testing.T) {+ _, err := ReadFrom(strings.NewReader(INVALID_FRONT_MATTER_LEADING_WS), "invalid/front/matter/leading/ws")
+ if err != nil {+ t.Fatalf("Unable to parse front matter given leading whitespace: %s", err)+ }
+}
--- a/hugolib/path_seperators_test.go
+++ b/hugolib/path_seperators_test.go
@@ -2,17 +2,27 @@
import (
"path/filepath"
+ "strings"
"testing"
)
+var SIMPLE_PAGE_YAML = `---
+contenttype: ""
+---
+Sample Text
+`
+
func TestDegenerateMissingFolderInPageFilename(t *testing.T) {- p := NewPage(filepath.Join("foobar"))+ p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE_YAML), filepath.Join("foobar"))+ if err != nil {+ t.Fatalf("Error in ReadFrom")+ }
if p.Section != "" { t.Fatalf("No section should be set for a file path: foobar")}
}
-func TestCreateNewPage(t *testing.T) {+func TestNewPageWithFilePath(t *testing.T) { toCheck := []map[string]string{ {"input": filepath.Join("sub", "foobar.html"), "expect": "sub"}, {"input": filepath.Join("content", "sub", "foobar.html"), "expect": "sub"},@@ -20,13 +30,15 @@
}
for _, el := range toCheck {- p := NewPage(el["input"])
+ p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE_YAML), el["input"])
+ if err != nil {+ t.Fatalf("Reading from SIMPLE_PAGE_YAML resulted in an error: %s", err)+ }
if p.Section != el["expect"] { t.Fatalf("Section not set to %s for page %s. Got: %s", el["expect"], el["input"], p.Section)}
}
}
-
func TestSettingOutFileOnPageContainsCorrectSlashes(t *testing.T) { s := NewSite(&Config{})--
⑨