Fixup tests and move metadata into own package

This commit is contained in:
Tobias Weingartner 2016-04-16 14:45:32 -07:00
parent 48d294a695
commit 7c9867917a
9 changed files with 330 additions and 106 deletions

View File

@ -70,9 +70,26 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
continue
}
var dirents []os.FileInfo
fpath := r.URL.Path
if idx, ok := middleware.IndexFile(md.FileSys, fpath, md.IndexFiles); ok {
// We're serving a directory index file, which may be a markdown
// file with a template. Let's grab a list of files this directory
// URL points to, and pass that in to any possible template invocations,
// so that templates can customize the look and feel of a directory.
fdp, err := md.FileSys.Open(fpath)
if err != nil {
return http.StatusInternalServerError, err
}
dirents, err = fdp.Readdir(-1)
if err != nil {
return http.StatusInternalServerError, err
}
// Set path to found index file
fpath = idx
_ = dirents
}
// If supported extension, process it
@ -100,11 +117,16 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
Req: r,
URL: r.URL,
}
html, err := md.Process(cfg, fpath, body, ctx)
html, err := cfg.Markdown(fpath, body, ctx)
if err != nil {
return http.StatusInternalServerError, err
}
// html, err = md.doTemplate(cfg, html, ctx)
// if err != nil {
// return http.StatusInternalServerError, err
// }
middleware.SetLastModifiedHeader(w, fs.ModTime())
w.Write(html)
return http.StatusOK, nil

View File

@ -1,6 +1,7 @@
package markdown
package metadata
import (
"bufio"
"bytes"
"fmt"
"time"
@ -64,20 +65,23 @@ func (m *Metadata) load(parsedMap map[string]interface{}) {
// MetadataParser is a an interface that must be satisfied by each parser
type MetadataParser interface {
// Initialize a parser
Init(b *bytes.Buffer) bool
// Type of metadata
Type() string
// Opening identifier
Opening() []byte
// Closing identifier
Closing() []byte
// Parse the metadata.
// Returns the remaining page contents (Markdown)
// after extracting metadata
Parse([]byte) ([]byte, error)
// Parsed metadata.
// Should be called after a call to Parse returns no error
Metadata() Metadata
// Raw markdown.
Markdown() []byte
}
// extractMetadata separates metadata content from from markdown content in b.
@ -109,8 +113,19 @@ func extractMetadata(parser MetadataParser, b []byte) (metadata []byte, markdown
return metadata, markdown, nil
}
func GetParser(buf []byte) MetadataParser {
for _, p := range parsers() {
b := bytes.NewBuffer(buf)
if p.Init(b) {
return p
}
}
return nil
}
// findParser finds the parser using line that contains opening identifier
func findParser(b []byte) MetadataParser {
func FindParser(b []byte) MetadataParser {
var line []byte
// Read first line
if _, err := fmt.Fscanln(bytes.NewReader(b), &line); err != nil {
@ -125,7 +140,7 @@ func findParser(b []byte) MetadataParser {
return nil
}
func newMetadata() Metadata {
func NewMetadata() Metadata {
return Metadata{
Variables: make(map[string]string),
Flags: make(map[string]bool),
@ -135,8 +150,53 @@ func newMetadata() Metadata {
// parsers returns all available parsers
func parsers() []MetadataParser {
return []MetadataParser{
&JSONMetadataParser{metadata: newMetadata()},
&TOMLMetadataParser{metadata: newMetadata()},
&YAMLMetadataParser{metadata: newMetadata()},
&TOMLMetadataParser{},
&YAMLMetadataParser{metadata: NewMetadata()},
&JSONMetadataParser{},
&NoneMetadataParser{},
}
}
// Split out "normal" metadata with given delimiter
func splitBuffer(b *bytes.Buffer, delim string) (*bytes.Buffer, *bytes.Buffer) {
scanner := bufio.NewScanner(b)
// Read and check first line
if !scanner.Scan() {
return nil, nil
}
if string(bytes.TrimSpace(scanner.Bytes())) != delim {
return nil, nil
}
// Accumulate metadata, until delimiter
meta := bytes.NewBuffer(nil)
for scanner.Scan() {
if string(bytes.TrimSpace(scanner.Bytes())) == delim {
break
}
if _, err := meta.Write(scanner.Bytes()); err != nil {
return nil, nil
}
if _, err := meta.WriteRune('\n'); err != nil {
return nil, nil
}
}
// Make sure we saw closing delimiter
if string(bytes.TrimSpace(scanner.Bytes())) != delim {
return nil, nil
}
// The rest is markdown
markdown := new(bytes.Buffer)
for scanner.Scan() {
if _, err := markdown.Write(scanner.Bytes()); err != nil {
return nil, nil
}
if _, err := markdown.WriteRune('\n'); err != nil {
return nil, nil
}
}
return meta, markdown
}

View File

@ -1,4 +1,4 @@
package markdown
package metadata
import (
"bytes"
@ -8,6 +8,39 @@ import (
// JSONMetadataParser is the MetadataParser for JSON
type JSONMetadataParser struct {
metadata Metadata
markdown *bytes.Buffer
}
func (j *JSONMetadataParser) Type() string {
return "JSON"
}
// Parse metadata/markdown file
func (j *JSONMetadataParser) Init(b *bytes.Buffer) bool {
m := make(map[string]interface{})
err := json.Unmarshal(b.Bytes(), &m)
if err != nil {
var offset int
if jerr, ok := err.(*json.SyntaxError); !ok {
return false
} else {
offset = int(jerr.Offset)
}
m = make(map[string]interface{})
err = json.Unmarshal(b.Next(offset-1), &m)
if err != nil {
return false
}
}
j.metadata = NewMetadata()
j.metadata.load(m)
j.markdown = bytes.NewBuffer(b.Bytes())
return true
}
// Parse the metadata
@ -34,6 +67,10 @@ func (j *JSONMetadataParser) Metadata() Metadata {
return j.metadata
}
func (j *JSONMetadataParser) Markdown() []byte {
return j.markdown.Bytes()
}
// Opening returns the opening identifier JSON metadata
func (j *JSONMetadataParser) Opening() []byte {
return []byte("{")

View File

@ -0,0 +1,50 @@
package metadata
import (
"bytes"
)
// TOMLMetadataParser is the MetadataParser for TOML
type NoneMetadataParser struct {
metadata Metadata
markdown *bytes.Buffer
}
func (n *NoneMetadataParser) Type() string {
return "None"
}
// Parse metadata/markdown file
func (n *NoneMetadataParser) Init(b *bytes.Buffer) bool {
m := make(map[string]interface{})
n.metadata = NewMetadata()
n.metadata.load(m)
n.markdown = bytes.NewBuffer(b.Bytes())
return true
}
// Parse the metadata
func (n *NoneMetadataParser) Parse(b []byte) ([]byte, error) {
return nil, nil
}
// Metadata returns parsed metadata. It should be called
// only after a call to Parse returns without error.
func (n *NoneMetadataParser) Metadata() Metadata {
return n.metadata
}
func (n *NoneMetadataParser) Markdown() []byte {
return n.markdown.Bytes()
}
// Opening returns the opening identifier TOML metadata
func (n *NoneMetadataParser) Opening() []byte {
return []byte("...")
}
// Closing returns the closing identifier TOML metadata
func (n *NoneMetadataParser) Closing() []byte {
return []byte("...")
}

View File

@ -1,9 +1,7 @@
package markdown
package metadata
import (
"bytes"
"fmt"
"reflect"
"strings"
"testing"
)
@ -164,56 +162,52 @@ func TestParsers(t *testing.T) {
testData [5]string
name string
}{
{&JSONMetadataParser{metadata: newMetadata()}, JSON, "json"},
{&YAMLMetadataParser{metadata: newMetadata()}, YAML, "yaml"},
{&TOMLMetadataParser{metadata: newMetadata()}, TOML, "toml"},
{&JSONMetadataParser{}, JSON, "JSON"},
{&YAMLMetadataParser{}, YAML, "YAML"},
{&TOMLMetadataParser{}, TOML, "TOML"},
}
for _, v := range data {
// metadata without identifiers
if _, err := v.parser.Parse([]byte(v.testData[0])); err == nil {
if v.parser.Init(bytes.NewBufferString(v.testData[0])) {
t.Fatalf("Expected error for invalid metadata for %v", v.name)
}
// metadata with identifiers
md, err := v.parser.Parse([]byte(v.testData[1]))
check(t, err)
if !v.parser.Init(bytes.NewBufferString(v.testData[1])) {
t.Fatalf("Metadata failed to initialize, type %v", v.parser.Type())
}
md := v.parser.Markdown()
if !compare(v.parser.Metadata()) {
t.Fatalf("Expected %v, found %v for %v", expected, v.parser.Metadata(), v.name)
}
if "Page content" != strings.TrimSpace(string(md)) {
t.Fatalf("Expected %v, found %v for %v", "Page content", string(md), v.name)
}
var line []byte
fmt.Fscanln(bytes.NewReader([]byte(v.testData[1])), &line)
if parser := findParser(line); parser == nil {
t.Fatalf("Parser must be found for %v", v.name)
} else {
if reflect.TypeOf(parser) != reflect.TypeOf(v.parser) {
t.Fatalf("parsers not equal. %v != %v", reflect.TypeOf(parser), reflect.TypeOf(v.parser))
}
// Check that we find the correct metadata parser type
if p := GetParser([]byte(v.testData[1])); p.Type() != v.name {
t.Fatalf("Wrong parser found, expected %v, found %v", v.name, p.Type())
}
// metadata without closing identifier
if _, err := v.parser.Parse([]byte(v.testData[2])); err == nil {
t.Fatalf("Expected error for missing closing identifier for %v", v.name)
if v.parser.Init(bytes.NewBufferString(v.testData[2])) {
t.Fatalf("Expected error for missing closing identifier for %v parser", v.name)
}
// invalid metadata
if _, err = v.parser.Parse([]byte(v.testData[3])); err == nil {
if v.parser.Init(bytes.NewBufferString(v.testData[3])) {
t.Fatalf("Expected error for invalid metadata for %v", v.name)
}
// front matter but no body
if _, err = v.parser.Parse([]byte(v.testData[4])); err != nil {
if !v.parser.Init(bytes.NewBufferString(v.testData[4])) {
t.Fatalf("Unexpected error for valid metadata but no body for %v", v.name)
}
}
}
func TestLargeBody(t *testing.T) {
var JSON = `{
"template": "chapter"
}
@ -232,24 +226,36 @@ Mycket olika byggnader har man i de nordiska rikena: pyramidformiga, kilformiga,
template : chapter
---
Mycket olika byggnader har man i de nordiska rikena: pyramidformiga, kilformiga, välvda, runda och fyrkantiga. De pyramidformiga består helt enkelt av träribbor, som upptill löper samman och nedtill bildar en vidare krets; de är avsedda att användas av hantverkarna under sommaren, för att de inte ska plågas av solen, samma gång som de besväras av rök och eld. De kilformiga husen är i regel försedda med höga tak, för att de täta och tunga snömassorna fortare ska kunna blåsa av och inte tynga ned taken. Dessa är täckta av björknäver, tegel eller kluvet spån av furu - för kådans skull -, gran, ek eller bok; taken de förmögnas hus däremot med plåtar av koppar eller bly, i likhet med kyrktaken. Valvbyggnaderna uppförs ganska konstnärligt till skydd mot våldsamma vindar och snöfall, görs av sten eller trä, och är avsedda för olika alldagliga viktiga ändamål. Liknande byggnader kan finnas i stormännens gårdar där de används som förvaringsrum för husgeråd och jordbruksredskap. De runda byggnaderna - som för övrigt är de högst sällsynta - används av konstnärer, som vid sitt arbete behöver ett jämnt fördelat ljus från taket. Vanligast är de fyrkantiga husen, vars grova bjälkar är synnerligen väl hopfogade i hörnen - ett sant mästerverk av byggnadskonst; även dessa har fönster högt uppe i taken, för att dagsljuset skall kunna strömma in och ge alla därinne full belysning. Stenhusen har dörröppningar i förhållande till byggnadens storlek, men smala fönstergluggar, som skydd mot den stränga kölden, frosten och snön. Vore de större och vidare, såsom fönstren i Italien, skulle husen i följd av den fint yrande snön, som röres upp av den starka blåsten, precis som dammet av virvelvinden, snart nog fyllas med massor av snö och inte kunna stå emot dess tryck, utan störta samman.
`
var NONE = `
Mycket olika byggnader har man i de nordiska rikena: pyramidformiga, kilformiga, välvda, runda och fyrkantiga. De pyramidformiga består helt enkelt av träribbor, som upptill löper samman och nedtill bildar en vidare krets; de är avsedda att användas av hantverkarna under sommaren, för att de inte ska plågas av solen, samma gång som de besväras av rök och eld. De kilformiga husen är i regel försedda med höga tak, för att de täta och tunga snömassorna fortare ska kunna blåsa av och inte tynga ned taken. Dessa är täckta av björknäver, tegel eller kluvet spån av furu - för kådans skull -, gran, ek eller bok; taken de förmögnas hus däremot med plåtar av koppar eller bly, i likhet med kyrktaken. Valvbyggnaderna uppförs ganska konstnärligt till skydd mot våldsamma vindar och snöfall, görs av sten eller trä, och är avsedda för olika alldagliga viktiga ändamål. Liknande byggnader kan finnas i stormännens gårdar där de används som förvaringsrum för husgeråd och jordbruksredskap. De runda byggnaderna - som för övrigt är de högst sällsynta - används av konstnärer, som vid sitt arbete behöver ett jämnt fördelat ljus från taket. Vanligast är de fyrkantiga husen, vars grova bjälkar är synnerligen väl hopfogade i hörnen - ett sant mästerverk av byggnadskonst; även dessa har fönster högt uppe i taken, för att dagsljuset skall kunna strömma in och ge alla därinne full belysning. Stenhusen har dörröppningar i förhållande till byggnadens storlek, men smala fönstergluggar, som skydd mot den stränga kölden, frosten och snön. Vore de större och vidare, såsom fönstren i Italien, skulle husen i följd av den fint yrande snön, som röres upp av den starka blåsten, precis som dammet av virvelvinden, snart nog fyllas med massor av snö och inte kunna stå emot dess tryck, utan störta samman.
`
var expectedBody = `Mycket olika byggnader har man i de nordiska rikena: pyramidformiga, kilformiga, välvda, runda och fyrkantiga. De pyramidformiga består helt enkelt av träribbor, som upptill löper samman och nedtill bildar en vidare krets; de är avsedda att användas av hantverkarna under sommaren, för att de inte ska plågas av solen, samma gång som de besväras av rök och eld. De kilformiga husen är i regel försedda med höga tak, för att de täta och tunga snömassorna fortare ska kunna blåsa av och inte tynga ned taken. Dessa är täckta av björknäver, tegel eller kluvet spån av furu - för kådans skull -, gran, ek eller bok; taken de förmögnas hus däremot med plåtar av koppar eller bly, i likhet med kyrktaken. Valvbyggnaderna uppförs ganska konstnärligt till skydd mot våldsamma vindar och snöfall, görs av sten eller trä, och är avsedda för olika alldagliga viktiga ändamål. Liknande byggnader kan finnas i stormännens gårdar där de används som förvaringsrum för husgeråd och jordbruksredskap. De runda byggnaderna - som för övrigt är de högst sällsynta - används av konstnärer, som vid sitt arbete behöver ett jämnt fördelat ljus från taket. Vanligast är de fyrkantiga husen, vars grova bjälkar är synnerligen väl hopfogade i hörnen - ett sant mästerverk av byggnadskonst; även dessa har fönster högt uppe i taken, för att dagsljuset skall kunna strömma in och ge alla därinne full belysning. Stenhusen har dörröppningar i förhållande till byggnadens storlek, men smala fönstergluggar, som skydd mot den stränga kölden, frosten och snön. Vore de större och vidare, såsom fönstren i Italien, skulle husen i följd av den fint yrande snön, som röres upp av den starka blåsten, precis som dammet av virvelvinden, snart nog fyllas med massor av snö och inte kunna stå emot dess tryck, utan störta samman.
`
data := []struct {
parser MetadataParser
pType string
testData string
name string
}{
{&JSONMetadataParser{metadata: newMetadata()}, JSON, "json"},
{&YAMLMetadataParser{metadata: newMetadata()}, YAML, "yaml"},
{&TOMLMetadataParser{metadata: newMetadata()}, TOML, "toml"},
{"JSON", JSON},
{"TOML", TOML},
{"YAML", YAML},
{"None", NONE},
}
for _, v := range data {
// metadata without identifiers
if md, err := v.parser.Parse([]byte(v.testData)); err != nil || strings.TrimSpace(string(md)) != strings.TrimSpace(expectedBody) {
t.Fatalf("Error not expected and/or markdown not equal for %v", v.name)
p := GetParser([]byte(v.testData))
if v.pType != p.Type() {
t.Fatalf("Wrong parser type, expected %v, got %v", v.pType, p.Type())
}
md := p.Markdown()
if strings.TrimSpace(string(md)) != strings.TrimSpace(expectedBody) {
t.Log("Provided:", v.testData)
t.Log("Returned:", p.Markdown())
t.Fatalf("Error, mismatched body in expected type %v, matched type %v", v.pType, p.Type())
}
}
}

View File

@ -1,12 +1,37 @@
package markdown
package metadata
import (
"bytes"
"github.com/BurntSushi/toml"
)
// TOMLMetadataParser is the MetadataParser for TOML
type TOMLMetadataParser struct {
metadata Metadata
markdown *bytes.Buffer
}
func (t *TOMLMetadataParser) Type() string {
return "TOML"
}
// Parse metadata/markdown file
func (t *TOMLMetadataParser) Init(b *bytes.Buffer) bool {
meta, data := splitBuffer(b, "+++")
if meta == nil || data == nil {
return false
}
t.markdown = data
m := make(map[string]interface{})
if err := toml.Unmarshal(meta.Bytes(), &m); err != nil {
return false
}
t.metadata = NewMetadata()
t.metadata.load(m)
return true
}
// Parse the metadata
@ -15,6 +40,7 @@ func (t *TOMLMetadataParser) Parse(b []byte) ([]byte, error) {
if err != nil {
return markdown, err
}
m := make(map[string]interface{})
if err := toml.Unmarshal(b, &m); err != nil {
return markdown, err
@ -29,6 +55,10 @@ func (t *TOMLMetadataParser) Metadata() Metadata {
return t.metadata
}
func (t *TOMLMetadataParser) Markdown() []byte {
return t.markdown.Bytes()
}
// Opening returns the opening identifier TOML metadata
func (t *TOMLMetadataParser) Opening() []byte {
return []byte("+++")

View File

@ -1,12 +1,36 @@
package markdown
package metadata
import (
"bytes"
"gopkg.in/yaml.v2"
)
// YAMLMetadataParser is the MetadataParser for YAML
type YAMLMetadataParser struct {
metadata Metadata
markdown *bytes.Buffer
}
func (y *YAMLMetadataParser) Type() string {
return "YAML"
}
func (y *YAMLMetadataParser) Init(b *bytes.Buffer) bool {
meta, data := splitBuffer(b, "---")
if meta == nil || data == nil {
return false
}
y.markdown = data
m := make(map[string]interface{})
if err := yaml.Unmarshal(meta.Bytes(), &m); err != nil {
return false
}
y.metadata = NewMetadata()
y.metadata.load(m)
return true
}
// Parse the metadata
@ -30,6 +54,10 @@ func (y *YAMLMetadataParser) Metadata() Metadata {
return y.metadata
}
func (y *YAMLMetadataParser) Markdown() []byte {
return y.markdown.Bytes()
}
// Opening returns the opening identifier YAML metadata
func (y *YAMLMetadataParser) Opening() []byte {
return []byte("---")

View File

@ -1,49 +1,19 @@
package markdown
import (
"bytes"
"path/filepath"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/markdown/metadata"
"github.com/russross/blackfriday"
)
// Data represents a markdown document.
type Data struct {
middleware.Context
Doc map[string]string
DocFlags map[string]bool
Styles []string
Scripts []string
}
// Include "overrides" the embedded middleware.Context's Include()
// method so that included files have access to d's fields.
func (d Data) Include(filename string) (string, error) {
return middleware.ContextInclude(filename, d, d.Root)
}
// Process processes the contents of a page in b. It parses the metadata
// Markdown processes the contents of a page in b. It parses the metadata
// (if any) and uses the template (if found).
func (md Markdown) Process(c *Config, requestPath string, b []byte, ctx middleware.Context) ([]byte, error) {
var metadata = newMetadata()
var markdown []byte
var err error
// find parser compatible with page contents
parser := findParser(b)
if parser == nil {
// if not found, assume whole file is markdown (no front matter)
markdown = b
} else {
// if found, assume metadata present and parse.
markdown, err = parser.Parse(b)
if err != nil {
return nil, err
}
metadata = parser.Metadata()
}
func (c *Config) Markdown(requestPath string, b []byte, ctx middleware.Context) ([]byte, error) {
parser := metadata.GetParser(b)
markdown := parser.Markdown()
mdata := parser.Metadata()
// process markdown
extns := 0
@ -54,33 +24,14 @@ func (md Markdown) Process(c *Config, requestPath string, b []byte, ctx middlewa
markdown = blackfriday.Markdown(markdown, c.Renderer, extns)
// set it as body for template
metadata.Variables["body"] = string(markdown)
title := metadata.Title
mdata.Variables["body"] = string(markdown)
title := mdata.Title
if title == "" {
title = filepath.Base(requestPath)
var extension = filepath.Ext(requestPath)
title = title[0 : len(title)-len(extension)]
}
metadata.Variables["title"] = title
mdata.Variables["title"] = title
// return md.processTemplate(c, requestPath, metadata, ctx)
return md.doTemplate(c, requestPath, metadata, ctx)
}
// doTemplate executes a template given a requestPath, template, and metadata
func (md Markdown) doTemplate(c *Config, reqPath string, metadata Metadata, ctx middleware.Context) ([]byte, error) {
mdData := Data{
Context: ctx,
Doc: metadata.Variables,
DocFlags: metadata.Flags,
Styles: c.Styles,
Scripts: c.Scripts,
}
b := new(bytes.Buffer)
if err := c.Template.ExecuteTemplate(b, metadata.Template, mdData); err != nil {
return nil, err
}
return b.Bytes(), nil
return execTemplate(c, mdata, ctx)
}

View File

@ -1,10 +1,50 @@
package markdown
import (
"bytes"
"io/ioutil"
"os"
"text/template"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/markdown/metadata"
)
// Data represents a markdown document.
type Data struct {
middleware.Context
Doc map[string]string
DocFlags map[string]bool
Styles []string
Scripts []string
Files []os.FileInfo
}
// Include "overrides" the embedded middleware.Context's Include()
// method so that included files have access to d's fields.
// Note: using {{template 'template-name' .}} instead might be better.
func (d Data) Include(filename string) (string, error) {
return middleware.ContextInclude(filename, d, d.Root)
}
// execTemplate executes a template given a requestPath, template, and metadata
func execTemplate(c *Config, mdata metadata.Metadata, ctx middleware.Context) ([]byte, error) {
mdData := Data{
Context: ctx,
Doc: mdata.Variables,
DocFlags: mdata.Flags,
Styles: c.Styles,
Scripts: c.Scripts,
}
b := new(bytes.Buffer)
if err := c.Template.ExecuteTemplate(b, mdata.Template, mdData); err != nil {
return nil, err
}
return b.Bytes(), nil
}
func setDefaultTemplate(filename string) *template.Template {
buf, err := ioutil.ReadFile(filename)
if err != nil {