shithub: hugo

Download patch

ref: 085ce15f7c041ba71772a0cd6eed4f06b6b0ca34
parent: 274d324c8bb818a76ffc5c35afcc33f4cf9eb5c3
author: Noah Campbell <[email protected]>
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{})