Commit bf47951f authored by Matt Holt's avatar Matt Holt

Merge pull request #196 from evermax/master

markdown, browse: Integrated Context struct for templating
parents 6006de57 604c8abb
......@@ -2,8 +2,8 @@ package setup
import (
"fmt"
"html/template"
"io/ioutil"
"text/template"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/browse"
......
......@@ -62,7 +62,8 @@ func Markdown(c *Controller) (middleware.Middleware, error) {
reqPath = "/" + reqPath
// Generate the static file
_, err = md.Process(cfg, reqPath, body)
ctx := middleware.Context{Root: md.FileSys}
_, err = md.Process(cfg, reqPath, body, ctx)
if err != nil {
return err
}
......
......@@ -5,13 +5,13 @@ package browse
import (
"bytes"
"errors"
"html/template"
"net/http"
"net/url"
"os"
"path"
"sort"
"strings"
"text/template"
"time"
"github.com/dustin/go-humanize"
......@@ -51,6 +51,8 @@ type Listing struct {
// And which order
Order string
middleware.Context
}
// FileInfo is the info about a particular file or directory
......@@ -137,8 +139,9 @@ var IndexPages = []string{
"default.txt",
}
func directoryListing(files []os.FileInfo, urlPath string, canGoUp bool) (Listing, error) {
func directoryListing(files []os.FileInfo, r *http.Request, canGoUp bool, root string) (Listing, error) {
var fileinfos []FileInfo
var urlPath = r.URL.Path
for _, f := range files {
name := f.Name()
......@@ -170,6 +173,11 @@ func directoryListing(files []os.FileInfo, urlPath string, canGoUp bool) (Listin
Path: urlPath,
CanGoUp: canGoUp,
Items: fileinfos,
Context: middleware.Context{
Root: http.Dir(root),
Req: r,
URL: r.URL,
},
}, nil
}
......@@ -224,7 +232,7 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
}
}
// Assemble listing of directory contents
listing, err := directoryListing(files, r.URL.Path, canGoUp)
listing, err := directoryListing(files, r, canGoUp, b.Root)
if err != nil { // directory isn't browsable
continue
}
......
package browse
import (
"net/http"
"net/http/httptest"
"sort"
"testing"
"text/template"
"time"
"github.com/mholt/caddy/middleware"
)
// "sort" package has "IsSorted" function, but no "IsReversed";
......@@ -94,3 +99,59 @@ func TestSort(t *testing.T) {
t.Errorf("The listing isn't reversed by time: %v", listing.Items)
}
}
func TestBrowseTemplate(t *testing.T) {
tmpl, err := template.ParseFiles("testdata/photos.tpl")
if err != nil {
t.Fatalf("An error occured while parsing the template: %v", err)
}
b := Browse{
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Next shouldn't be called")
return 0, nil
}),
Root: "./testdata",
Configs: []Config{
Config{
PathScope: "/photos",
Template: tmpl,
},
},
}
req, err := http.NewRequest("GET", "/photos/", nil)
if err != nil {
t.Fatalf("Test: Could not create HTTP request: %v", err)
}
rec := httptest.NewRecorder()
b.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, rec.Code)
}
respBody := rec.Body.String()
expectedBody := `<!DOCTYPE html>
<html>
<head>
<title>Template</title>
</head>
<body>
<h1>Header</h1>
<h1>/photos/</h1>
<a href="test.html">test.html</a><br>
<a href="test2.html">test2.html</a><br>
</body>
</html>
`
if respBody != expectedBody {
t.Fatalf("Expected body: %v got: %v", expectedBody, respBody)
}
}
<!DOCTYPE html>
<html>
<head>
<title>Template</title>
</head>
<body>
{{.Include "header.html"}}
<h1>{{.Path}}</h1>
{{range .Items}}
<a href="{{.URL}}">{{.Name}}</a><br>
{{end}}
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Test 2</title>
</head>
<body>
</body>
</html>
package templates
package middleware
import (
"bytes"
......@@ -8,23 +8,22 @@ import (
"net/url"
"text/template"
"time"
"github.com/mholt/caddy/middleware"
)
// This file contains the context and functions available for
// use in the templates.
// context is the context with which templates are executed.
type context struct {
root http.FileSystem
req *http.Request
URL *url.URL
type Context struct {
Root http.FileSystem
Req *http.Request
// This is used to access information about the URL.
URL *url.URL
}
// Include returns the contents of filename relative to the site root
func (c context) Include(filename string) (string, error) {
file, err := c.root.Open(filename)
func (c Context) Include(filename string) (string, error) {
file, err := c.Root.Open(filename)
if err != nil {
return "", err
}
......@@ -50,13 +49,13 @@ func (c context) Include(filename string) (string, error) {
}
// Date returns the current timestamp in the specified format
func (c context) Date(format string) string {
func (c Context) Date(format string) string {
return time.Now().Format(format)
}
// Cookie gets the value of a cookie with name name.
func (c context) Cookie(name string) string {
cookies := c.req.Cookies()
func (c Context) Cookie(name string) string {
cookies := c.Req.Cookies()
for _, cookie := range cookies {
if cookie.Name == name {
return cookie.Value
......@@ -66,15 +65,15 @@ func (c context) Cookie(name string) string {
}
// Header gets the value of a request header with field name.
func (c context) Header(name string) string {
return c.req.Header.Get(name)
func (c Context) Header(name string) string {
return c.Req.Header.Get(name)
}
// IP gets the (remote) IP address of the client making the request.
func (c context) IP() string {
ip, _, err := net.SplitHostPort(c.req.RemoteAddr)
func (c Context) IP() string {
ip, _, err := net.SplitHostPort(c.Req.RemoteAddr)
if err != nil {
return c.req.RemoteAddr
return c.Req.RemoteAddr
}
return ip
}
......@@ -82,14 +81,14 @@ func (c context) IP() string {
// URI returns the raw, unprocessed request URI (including query
// string and hash) obtained directly from the Request-Line of
// the HTTP request.
func (c context) URI() string {
return c.req.RequestURI
func (c Context) URI() string {
return c.Req.RequestURI
}
// Host returns the hostname portion of the Host header
// from the HTTP request.
func (c context) Host() (string, error) {
host, _, err := net.SplitHostPort(c.req.Host)
func (c Context) Host() (string, error) {
host, _, err := net.SplitHostPort(c.Req.Host)
if err != nil {
return "", err
}
......@@ -97,8 +96,8 @@ func (c context) Host() (string, error) {
}
// Port returns the port portion of the Host header if specified.
func (c context) Port() (string, error) {
_, port, err := net.SplitHostPort(c.req.Host)
func (c Context) Port() (string, error) {
_, port, err := net.SplitHostPort(c.Req.Host)
if err != nil {
return "", err
}
......@@ -106,12 +105,12 @@ func (c context) Port() (string, error) {
}
// Method returns the method (GET, POST, etc.) of the request.
func (c context) Method() string {
return c.req.Method
func (c Context) Method() string {
return c.Req.Method
}
// PathMatches returns true if the path portion of the request
// URL matches pattern.
func (c context) PathMatches(pattern string) bool {
return middleware.Path(c.req.URL.Path).Matches(pattern)
func (c Context) PathMatches(pattern string) bool {
return Path(c.Req.URL.Path).Matches(pattern)
}
......@@ -119,7 +119,12 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
return http.StatusInternalServerError, err
}
html, err := md.Process(m, fpath, body)
ctx := middleware.Context{
Root: md.FileSys,
Req: r,
URL: r.URL,
}
html, err := md.Process(m, fpath, body, ctx)
if err != nil {
return http.StatusInternalServerError, err
}
......
package markdown
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/mholt/caddy/middleware"
"github.com/russross/blackfriday"
)
func TestMarkdown(t *testing.T) {
templates := make(map[string]string)
templates[DefaultTemplate] = "testdata/markdown_tpl.html"
md := Markdown{
Root: "./testdata",
FileSys: http.Dir("./testdata"),
Configs: []Config{
Config{
Renderer: blackfriday.HtmlRenderer(0, "", ""),
PathScope: "/blog",
Extensions: []string{".md"},
Styles: []string{},
Scripts: []string{},
Templates: templates,
},
Config{
Renderer: blackfriday.HtmlRenderer(0, "", ""),
PathScope: "/log",
Extensions: []string{".md"},
Styles: []string{"/resources/css/log.css", "/resources/css/default.css"},
Scripts: []string{"/resources/js/log.js", "/resources/js/default.js"},
Templates: make(map[string]string),
},
},
IndexFiles: []string{"index.html"},
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Next shouldn't be called")
return 0, nil
}),
}
req, err := http.NewRequest("GET", "/blog/test.md", nil)
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
rec := httptest.NewRecorder()
md.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("Wrong status, expected: %d and got %d", http.StatusOK, rec.Code)
}
respBody := rec.Body.String()
expectedBody := `<!DOCTYPE html>
<html>
<head>
<title>Markdown test</title>
</head>
<body>
<h1>Header</h1>
Welcome to A Caddy website!
<h2>Welcome on the blog</h2>
<p>Body</p>
<p><code>go
func getTrue() bool {
return true
}
</code></p>
</body>
</html>
`
if respBody != expectedBody {
t.Fatalf("Expected body: %v got: %v", expectedBody, respBody)
}
req, err = http.NewRequest("GET", "/log/test.md", nil)
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
rec = httptest.NewRecorder()
md.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("Wrong status, expected: %d and got %d", http.StatusOK, rec.Code)
}
respBody = rec.Body.String()
expectedBody = `<!DOCTYPE html>
<html>
<head>
<title>Markdown test</title>
<meta charset="utf-8">
<link rel="stylesheet" href="/resources/css/log.css">
<link rel="stylesheet" href="/resources/css/default.css">
<script src="/resources/js/log.js"></script>
<script src="/resources/js/default.js"></script>
</head>
<body>
<h2>Welcome on the blog</h2>
<p>Body</p>
<p><code>go
func getTrue() bool {
return true
}
</code></p>
</body>
</html>`
replacer := strings.NewReplacer("\r", "", "\n", "")
respBody = replacer.Replace(respBody)
expectedBody = replacer.Replace(expectedBody)
if respBody != expectedBody {
t.Fatalf("Expected body: %v got: %v", expectedBody, respBody)
}
}
......@@ -9,6 +9,7 @@ import (
"strings"
"text/template"
"github.com/mholt/caddy/middleware"
"github.com/russross/blackfriday"
)
......@@ -17,9 +18,14 @@ const (
DefaultStaticDir = "generated_site"
)
type MarkdownData struct {
middleware.Context
Doc map[string]interface{}
}
// Process 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) ([]byte, error) {
func (md Markdown) Process(c Config, requestPath string, b []byte, ctx middleware.Context) ([]byte, error) {
var metadata = Metadata{Variables: make(map[string]interface{})}
var markdown []byte
var err error
......@@ -61,14 +67,21 @@ func (md Markdown) Process(c Config, requestPath string, b []byte) ([]byte, erro
markdown = blackfriday.Markdown(markdown, c.Renderer, 0)
// set it as body for template
metadata.Variables["markdown"] = string(markdown)
metadata.Variables["body"] = string(markdown)
title := metadata.Title
if title == "" {
title = filepath.Base(requestPath)
var extension = filepath.Ext(requestPath)
title = title[0 : len(title)-len(extension)]
}
metadata.Variables["title"] = title
return md.processTemplate(c, requestPath, tmpl, metadata)
return md.processTemplate(c, requestPath, tmpl, metadata, ctx)
}
// processTemplate processes a template given a requestPath,
// template (tmpl) and metadata
func (md Markdown) processTemplate(c Config, requestPath string, tmpl []byte, metadata Metadata) ([]byte, error) {
func (md Markdown) processTemplate(c Config, requestPath string, tmpl []byte, metadata Metadata, ctx middleware.Context) ([]byte, error) {
// if template is not specified,
// use the default template
if tmpl == nil {
......@@ -81,7 +94,12 @@ func (md Markdown) processTemplate(c Config, requestPath string, tmpl []byte, me
if err != nil {
return nil, err
}
if err = t.Execute(b, metadata.Variables); err != nil {
mdData := MarkdownData{
Context: ctx,
Doc: metadata.Variables,
}
if err = t.Execute(b, mdData); err != nil {
return nil, err
}
......@@ -148,15 +166,7 @@ func defaultTemplate(c Config, metadata Metadata, requestPath string) []byte {
}
// Title is first line (length-limited), otherwise filename
title := metadata.Title
if title == "" {
title = filepath.Base(requestPath)
if body, _ := metadata.Variables["markdown"].([]byte); len(body) > 128 {
title = string(body[:128])
} else if len(body) > 0 {
title = string(body)
}
}
title, _ := metadata.Variables["title"].(string)
html := []byte(htmlTemplate)
html = bytes.Replace(html, []byte("{{title}}"), []byte(title), 1)
......@@ -176,7 +186,7 @@ const (
{{js}}
</head>
<body>
{{.markdown}}
{{.Doc.body}}
</body>
</html>`
cssTemplate = `<link rel="stylesheet" href="{{url}}">`
......
---
title: Markdown test
variables:
sitename: A Caddy website
---
## Welcome on the blog
Body
``` go
func getTrue() bool {
return true
}
```
---
title: Markdown test
variables:
sitename: A Caddy website
---
## Welcome on the blog
Body
``` go
func getTrue() bool {
return true
}
```
<!DOCTYPE html>
<html>
<head>
<title>{{.Doc.title}}</title>
</head>
<body>
{{.Include "header.html"}}
Welcome to {{.Doc.sitename}}!
{{.Doc.body}}
</body>
</html>
......@@ -31,7 +31,7 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
for _, ext := range rule.Extensions {
if reqExt == ext {
// Create execution context
ctx := context{root: t.FileSys, req: r, URL: r.URL}
ctx := middleware.Context{Root: t.FileSys, Req: r, URL: r.URL}
// Build the template
tpl, err := template.ParseFiles(filepath.Join(t.Root, fpath))
......
package templates
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/mholt/caddy/middleware"
)
func Test(t *testing.T) {
tmpl := Templates{
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}),
Rules: []Rule{
Rule{
Extensions: []string{".html"},
IndexFiles: []string{"index.html"},
Path: "/photos",
},
Rule{
Extensions: []string{".html", ".htm"},
IndexFiles: []string{"index.html", "index.htm"},
Path: "/images",
},
},
Root: "./testdata",
FileSys: http.Dir("./testdata"),
}
tmplroot := Templates{
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}),
Rules: []Rule{
Rule{
Extensions: []string{".html"},
IndexFiles: []string{"index.html"},
Path: "/",
},
},
Root: "./testdata",
FileSys: http.Dir("./testdata"),
}
/*
* Test tmpl on /photos/test.html
*/
req, err := http.NewRequest("GET", "/photos/test.html", nil)
if err != nil {
t.Fatalf("Test: Could not create HTTP request: %v", err)
}
rec := httptest.NewRecorder()
tmpl.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("Test: Wrong response code: %d, should be %d", rec.Code, http.StatusOK)
}
respBody := rec.Body.String()
expectedBody := `<!DOCTYPE html><html><head><title>test page</title></head><body><h1>Header title</h1>
</body></html>
`
if respBody != expectedBody {
t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody)
}
/*
* Test tmpl on /images/img.htm
*/
req, err = http.NewRequest("GET", "/images/img.htm", nil)
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
rec = httptest.NewRecorder()
tmpl.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("Test: Wrong response code: %d, should be %d", rec.Code, http.StatusOK)
}
respBody = rec.Body.String()
expectedBody = `<!DOCTYPE html><html><head><title>img</title></head><body><h1>Header title</h1>
</body></html>
`
if respBody != expectedBody {
t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody)
}
/*
* Test tmplroot on /root.html
*/
req, err = http.NewRequest("GET", "/root.html", nil)
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
rec = httptest.NewRecorder()
tmplroot.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("Test: Wrong response code: %d, should be %d", rec.Code, http.StatusOK)
}
respBody = rec.Body.String()
expectedBody = `<!DOCTYPE html><html><head><title>root</title></head><body><h1>Header title</h1>
</body></html>
`
if respBody != expectedBody {
t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody)
}
}
<!DOCTYPE html><html><head><title>img</title></head><body>{{.Include "header.html"}}</body></html>
<!DOCTYPE html><html><head><title>test page</title></head><body>{{.Include "../header.html"}}</body></html>
<!DOCTYPE html><html><head><title>root</title></head><body>{{.Include "header.html"}}</body></html>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment