diff --git a/pages/file_test.go b/pages/file_test.go index 91e4867..9e68cee 100644 --- a/pages/file_test.go +++ b/pages/file_test.go @@ -1,6 +1,8 @@ package pages import ( + "bytes" + "fmt" "io" "testing" @@ -21,11 +23,15 @@ func (s siteFake) RendererManager() renderers.Renderers { return &renderManagerF type renderManagerFake struct{ t *testing.T } -func (rm renderManagerFake) ApplyLayout(layout string, src []byte, vars liquid.Bindings) ([]byte, error) { +func (rm renderManagerFake) ApplyLayout(layout string, content []byte, vars liquid.Bindings) ([]byte, error) { require.Equal(rm.t, "layout1", layout) - return nil, nil + return content, nil } + func (rm renderManagerFake) Render(w io.Writer, src []byte, vars liquid.Bindings, filename string, lineNo int) error { + if bytes.Contains(src, []byte("{% error %}")) { + return fmt.Errorf("render error") + } _, err := io.WriteString(w, "rendered: ") if err != nil { return err diff --git a/pages/page.go b/pages/page.go index aa7578b..d97fa64 100644 --- a/pages/page.go +++ b/pages/page.go @@ -12,6 +12,7 @@ import ( "time" "github.com/osteele/gojekyll/frontmatter" + "github.com/osteele/gojekyll/utils" "github.com/osteele/gojekyll/version" "github.com/osteele/liquid/evaluator" ) @@ -169,24 +170,23 @@ func (f *file) PostDate() time.Time { // Write applies Liquid and Markdown, as appropriate. func (p *page) Write(w io.Writer) error { - err := p.Render() - if err != nil { + if err := p.Render(); err != nil { return err } p.RLock() defer p.RUnlock() - content := p.content - layout, ok := p.frontMatter["layout"].(string) - if ok && layout != "" { - rp := p.site.RendererManager() - b, e := rp.ApplyLayout(layout, []byte(content), p.TemplateContext()) - if e != nil { - return e + cn := p.content + lo, ok := p.frontMatter["layout"].(string) + if ok && lo != "" { + rm := p.site.RendererManager() + b, err := rm.ApplyLayout(lo, []byte(cn), p.TemplateContext()) + if err != nil { + return err } _, err = w.Write(b) - } else { - _, err = io.WriteString(w, content) + return err } + _, err := io.WriteString(w, cn) return err } @@ -197,7 +197,7 @@ func (p *page) Render() error { p.Lock() defer p.Unlock() p.content = cn - p.contentError = err + p.contentError = utils.WrapPathError(err, p.filename) p.excerpt = ex p.rendered = true }) diff --git a/pages/page_test.go b/pages/page_test.go index 582cf5c..f476232 100644 --- a/pages/page_test.go +++ b/pages/page_test.go @@ -2,10 +2,14 @@ package pages import ( "bytes" + "io/ioutil" "os" + "path/filepath" "testing" "github.com/osteele/gojekyll/config" + "github.com/osteele/gojekyll/frontmatter" + "github.com/osteele/gojekyll/utils" "github.com/stretchr/testify/require" ) @@ -27,17 +31,43 @@ func TestPage_TemplateContext(t *testing.T) { func TestPage_Categories(t *testing.T) { s := siteFake{t, config.Default()} - fm := map[string]interface{}{"categories": "b a"} + fm := frontmatter.FrontMatter{"categories": "b a"} f := file{site: s, frontMatter: fm} p := page{file: f} require.Equal(t, []string{"a", "b"}, p.Categories()) } func TestPage_Write(t *testing.T) { - cfg := config.Default() - p, err := NewFile(siteFake{t, cfg}, "testdata/page_with_layout.md", "page_with_layout.md", map[string]interface{}{}) + t.Run("rendering", func(t *testing.T) { + p := requirePageFromFile(t, "page_with_layout.md") + buf := new(bytes.Buffer) + require.NoError(t, p.Write(buf)) + require.Contains(t, buf.String(), "page with layout") + }) + + t.Run("rendering error", func(t *testing.T) { + p := requirePageFromFile(t, "liquid_error.md") + err := p.Write(ioutil.Discard) + require.NotNil(t, err) + require.Contains(t, err.Error(), "render error") + pe, ok := err.(utils.PathError) + require.True(t, ok) + require.Equal(t, "testdata/liquid_error.md", pe.Path()) + }) +} + +func fakePageFromFile(t *testing.T, file string) (Document, error) { + return NewFile( + siteFake{t, config.Default()}, + filepath.Join("testdata", file), + file, + frontmatter.FrontMatter{}, + ) +} + +func requirePageFromFile(t *testing.T, file string) Document { + p, err := fakePageFromFile(t, file) require.NoError(t, err) require.NotNil(t, p) - buf := new(bytes.Buffer) - require.NoError(t, p.Write(buf)) + return p } diff --git a/pages/testdata/liquid_error.md b/pages/testdata/liquid_error.md new file mode 100644 index 0000000..84cabe3 --- /dev/null +++ b/pages/testdata/liquid_error.md @@ -0,0 +1,4 @@ +--- +--- + +{% error %} diff --git a/renderers/layouts.go b/renderers/layouts.go index fb6766c..52b2525 100644 --- a/renderers/layouts.go +++ b/renderers/layouts.go @@ -13,8 +13,8 @@ import ( "github.com/osteele/liquid" ) -// ApplyLayout applies the named layout to the data. -func (p *Manager) ApplyLayout(name string, data []byte, vars liquid.Bindings) ([]byte, error) { +// ApplyLayout applies the named layout to the content. +func (p *Manager) ApplyLayout(name string, content []byte, vars liquid.Bindings) ([]byte, error) { for name != "" { var lfm map[string]interface{} tpl, err := p.FindLayout(name, &lfm) @@ -22,16 +22,16 @@ func (p *Manager) ApplyLayout(name string, data []byte, vars liquid.Bindings) ([ return nil, err } b := utils.MergeStringMaps(vars, map[string]interface{}{ - "content": string(data), + "content": string(content), "layout": lfm, }) - data, err = tpl.Render(b) + content, err = tpl.Render(b) if err != nil { return nil, utils.WrapPathError(err, name) } name = templates.VariableMap(lfm).String("layout", "") } - return data, nil + return content, nil } // FindLayout returns a template for the named layout. diff --git a/renderers/markdown.go b/renderers/markdown.go index f705800..6ebe0c9 100644 --- a/renderers/markdown.go +++ b/renderers/markdown.go @@ -2,6 +2,7 @@ package renderers import ( "bytes" + "fmt" "io" "regexp" @@ -29,17 +30,31 @@ const blackfridayExtensions = 0 | // added relative to commonExtensions blackfriday.EXTENSION_AUTO_HEADER_IDS -func renderMarkdown(md []byte) []byte { +func renderMarkdown(md []byte) ([]byte, error) { renderer := blackfriday.HtmlRenderer(blackfridayFlags, "", "") - html := blackfriday.MarkdownOptions(md, renderer, blackfriday.Options{ - Extensions: blackfridayExtensions}) + html := blackfriday.MarkdownOptions( + md, + renderer, + blackfriday.Options{Extensions: blackfridayExtensions}, + ) html, err := renderInnerMarkdown(html) if err != nil { - panic(err) + return nil, fmt.Errorf("%s while rendering markdown", err) } - return html + return html, nil } +func _renderMarkdown(md []byte) ([]byte, error) { + renderer := blackfriday.HtmlRenderer(blackfridayFlags, "", "") + html := blackfriday.MarkdownOptions( + md, + renderer, + blackfriday.Options{Extensions: blackfridayExtensions}, + ) + return html, nil +} + +// search HTML for markdown=1, and process if found func renderInnerMarkdown(b []byte) ([]byte, error) { z := html.NewTokenizer(bytes.NewReader(b)) buf := new(bytes.Buffer) @@ -54,7 +69,7 @@ outer: return nil, z.Err() case html.StartTagToken: if hasMarkdownAttr(z) { - _, err := buf.Write(removeMarkdownAttr(z.Raw())) + _, err := buf.Write(stripMarkdownAttr(z.Raw())) if err != nil { return nil, err } @@ -62,6 +77,7 @@ outer: return nil, err } // the above leaves z set to the end token + // fall through to render it } } _, err := buf.Write(z.Raw()) @@ -72,8 +88,6 @@ outer: return buf.Bytes(), nil } -var markdownAttrRE = regexp.MustCompile(`\s*markdown\s*=\s*("1"|'1'|1)\s*`) - func hasMarkdownAttr(z *html.Tokenizer) bool { for { k, v, more := z.TagAttr() @@ -86,12 +100,18 @@ func hasMarkdownAttr(z *html.Tokenizer) bool { } } -func removeMarkdownAttr(tag []byte) []byte { +var markdownAttrRE = regexp.MustCompile(`\s*markdown\s*=[^\s>]*\s*`) + +// return the text of a start tag, w/out the markdown attribute +func stripMarkdownAttr(tag []byte) []byte { tag = markdownAttrRE.ReplaceAll(tag, []byte(" ")) tag = bytes.Replace(tag, []byte(" >"), []byte(">"), 1) return tag } +// called once markdown="1" attribute is detected. +// Collects the HTML tokens into a string, applies markdown to them, +// and writes the result func processInnerMarkdown(w io.Writer, z *html.Tokenizer) error { buf := new(bytes.Buffer) depth := 1 @@ -111,10 +131,13 @@ loop: } _, err := buf.Write(z.Raw()) if err != nil { - panic(err) + return err } } - html := renderMarkdown(buf.Bytes()) - _, err := w.Write(html) + html, err := _renderMarkdown(buf.Bytes()) + if err != nil { + return err + } + _, err = w.Write(html) return err } diff --git a/renderers/markdown_test.go b/renderers/markdown_test.go index dd65bac..c9a54a5 100644 --- a/renderers/markdown_test.go +++ b/renderers/markdown_test.go @@ -1,19 +1,35 @@ package renderers import ( + "log" "testing" "github.com/stretchr/testify/require" ) -func renderMarkdownString(s string) string { - return string(renderMarkdown([]byte(s))) +func TestRenderMarkdown(t *testing.T) { + require.Equal(t, "

b

\n", mustMarkdownString("*b*")) + require.Equal(t, "
*b*
\n", mustMarkdownString("
*b*
")) + require.Equal(t, "

b

\n
\n", mustMarkdownString(`
*b*
`)) + require.Equal(t, "

b

\n
\n", mustMarkdownString(`
*b*
`)) + require.Equal(t, "

b

\n
\n", mustMarkdownString(`
*b*
`)) + + _, err := renderMarkdownString(`

`) + require.NotNil(t, err) } -func TestRenderMarkdown(t *testing.T) { - require.Equal(t, "

b

\n", renderMarkdownString("*b*")) - require.Equal(t, "
*b*
\n", renderMarkdownString("
*b*
")) - require.Equal(t, "

b

\n
\n", renderMarkdownString(`
*b*
`)) - require.Equal(t, "

b

\n
\n", renderMarkdownString(`
*b*
`)) - require.Equal(t, "

b

\n
\n", renderMarkdownString(`
*b*
`)) +func mustMarkdownString(md string) string { + s, err := renderMarkdown([]byte(md)) + if err != nil { + log.Fatal(err) + } + return string(s) +} + +func renderMarkdownString(md string) (string, error) { + s, err := renderMarkdown([]byte(md)) + if err != nil { + return "", err + } + return string(s), err } diff --git a/renderers/renderers.go b/renderers/renderers.go index 232d8bd..75cf74f 100644 --- a/renderers/renderers.go +++ b/renderers/renderers.go @@ -64,7 +64,10 @@ func (p *Manager) Render(w io.Writer, src []byte, vars liquid.Bindings, filename return err } if p.cfg.IsMarkdown(filename) { - src = renderMarkdown(src) + src, err = renderMarkdown(src) + if err != nil { + return err + } } _, err = w.Write(src) return err diff --git a/utils/errors.go b/utils/errors.go index 7cee2cc..c8db807 100644 --- a/utils/errors.go +++ b/utils/errors.go @@ -19,8 +19,12 @@ type pathError struct { path string } -func (p *pathError) Error() string { - return fmt.Sprintf("%s: %s", p.path, p.cause) +func (pe *pathError) Error() string { + return fmt.Sprintf("%s: %s", pe.path, pe.cause) +} + +func (pe *pathError) Path() string { + return pe.path } // WrapPathError returns an error that will print with a path.\