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 ...@@ -2,8 +2,8 @@ package setup
import ( import (
"fmt" "fmt"
"html/template"
"io/ioutil" "io/ioutil"
"text/template"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/browse" "github.com/mholt/caddy/middleware/browse"
......
...@@ -62,7 +62,8 @@ func Markdown(c *Controller) (middleware.Middleware, error) { ...@@ -62,7 +62,8 @@ func Markdown(c *Controller) (middleware.Middleware, error) {
reqPath = "/" + reqPath reqPath = "/" + reqPath
// Generate the static file // 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 { if err != nil {
return err return err
} }
......
...@@ -5,13 +5,13 @@ package browse ...@@ -5,13 +5,13 @@ package browse
import ( import (
"bytes" "bytes"
"errors" "errors"
"html/template"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path" "path"
"sort" "sort"
"strings" "strings"
"text/template"
"time" "time"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
...@@ -51,6 +51,8 @@ type Listing struct { ...@@ -51,6 +51,8 @@ type Listing struct {
// And which order // And which order
Order string Order string
middleware.Context
} }
// FileInfo is the info about a particular file or directory // FileInfo is the info about a particular file or directory
...@@ -137,8 +139,9 @@ var IndexPages = []string{ ...@@ -137,8 +139,9 @@ var IndexPages = []string{
"default.txt", "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 fileinfos []FileInfo
var urlPath = r.URL.Path
for _, f := range files { for _, f := range files {
name := f.Name() name := f.Name()
...@@ -170,6 +173,11 @@ func directoryListing(files []os.FileInfo, urlPath string, canGoUp bool) (Listin ...@@ -170,6 +173,11 @@ func directoryListing(files []os.FileInfo, urlPath string, canGoUp bool) (Listin
Path: urlPath, Path: urlPath,
CanGoUp: canGoUp, CanGoUp: canGoUp,
Items: fileinfos, Items: fileinfos,
Context: middleware.Context{
Root: http.Dir(root),
Req: r,
URL: r.URL,
},
}, nil }, nil
} }
...@@ -224,7 +232,7 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { ...@@ -224,7 +232,7 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
} }
} }
// Assemble listing of directory contents // 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 if err != nil { // directory isn't browsable
continue continue
} }
......
package browse package browse
import ( import (
"net/http"
"net/http/httptest"
"sort" "sort"
"testing" "testing"
"text/template"
"time" "time"
"github.com/mholt/caddy/middleware"
) )
// "sort" package has "IsSorted" function, but no "IsReversed"; // "sort" package has "IsSorted" function, but no "IsReversed";
...@@ -94,3 +99,59 @@ func TestSort(t *testing.T) { ...@@ -94,3 +99,59 @@ func TestSort(t *testing.T) {
t.Errorf("The listing isn't reversed by time: %v", listing.Items) 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 ( import (
"bytes" "bytes"
...@@ -8,23 +8,22 @@ import ( ...@@ -8,23 +8,22 @@ import (
"net/url" "net/url"
"text/template" "text/template"
"time" "time"
"github.com/mholt/caddy/middleware"
) )
// This file contains the context and functions available for // This file contains the context and functions available for
// use in the templates. // use in the templates.
// context is the context with which templates are executed. // context is the context with which templates are executed.
type context struct { type Context struct {
root http.FileSystem Root http.FileSystem
req *http.Request Req *http.Request
URL *url.URL // This is used to access information about the URL.
URL *url.URL
} }
// Include returns the contents of filename relative to the site root // Include returns the contents of filename relative to the site root
func (c context) Include(filename string) (string, error) { func (c Context) Include(filename string) (string, error) {
file, err := c.root.Open(filename) file, err := c.Root.Open(filename)
if err != nil { if err != nil {
return "", err return "", err
} }
...@@ -50,13 +49,13 @@ func (c context) Include(filename string) (string, error) { ...@@ -50,13 +49,13 @@ func (c context) Include(filename string) (string, error) {
} }
// Date returns the current timestamp in the specified format // 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) return time.Now().Format(format)
} }
// Cookie gets the value of a cookie with name name. // Cookie gets the value of a cookie with name name.
func (c context) Cookie(name string) string { func (c Context) Cookie(name string) string {
cookies := c.req.Cookies() cookies := c.Req.Cookies()
for _, cookie := range cookies { for _, cookie := range cookies {
if cookie.Name == name { if cookie.Name == name {
return cookie.Value return cookie.Value
...@@ -66,15 +65,15 @@ func (c context) Cookie(name string) string { ...@@ -66,15 +65,15 @@ func (c context) Cookie(name string) string {
} }
// Header gets the value of a request header with field name. // Header gets the value of a request header with field name.
func (c context) Header(name string) string { func (c Context) Header(name string) string {
return c.req.Header.Get(name) return c.Req.Header.Get(name)
} }
// IP gets the (remote) IP address of the client making the request. // IP gets the (remote) IP address of the client making the request.
func (c context) IP() string { func (c Context) IP() string {
ip, _, err := net.SplitHostPort(c.req.RemoteAddr) ip, _, err := net.SplitHostPort(c.Req.RemoteAddr)
if err != nil { if err != nil {
return c.req.RemoteAddr return c.Req.RemoteAddr
} }
return ip return ip
} }
...@@ -82,14 +81,14 @@ func (c context) IP() string { ...@@ -82,14 +81,14 @@ func (c context) IP() string {
// URI returns the raw, unprocessed request URI (including query // URI returns the raw, unprocessed request URI (including query
// string and hash) obtained directly from the Request-Line of // string and hash) obtained directly from the Request-Line of
// the HTTP request. // the HTTP request.
func (c context) URI() string { func (c Context) URI() string {
return c.req.RequestURI return c.Req.RequestURI
} }
// Host returns the hostname portion of the Host header // Host returns the hostname portion of the Host header
// from the HTTP request. // from the HTTP request.
func (c context) Host() (string, error) { func (c Context) Host() (string, error) {
host, _, err := net.SplitHostPort(c.req.Host) host, _, err := net.SplitHostPort(c.Req.Host)
if err != nil { if err != nil {
return "", err return "", err
} }
...@@ -97,8 +96,8 @@ func (c context) Host() (string, error) { ...@@ -97,8 +96,8 @@ func (c context) Host() (string, error) {
} }
// Port returns the port portion of the Host header if specified. // Port returns the port portion of the Host header if specified.
func (c context) Port() (string, error) { func (c Context) Port() (string, error) {
_, port, err := net.SplitHostPort(c.req.Host) _, port, err := net.SplitHostPort(c.Req.Host)
if err != nil { if err != nil {
return "", err return "", err
} }
...@@ -106,12 +105,12 @@ func (c context) Port() (string, error) { ...@@ -106,12 +105,12 @@ func (c context) Port() (string, error) {
} }
// Method returns the method (GET, POST, etc.) of the request. // Method returns the method (GET, POST, etc.) of the request.
func (c context) Method() string { func (c Context) Method() string {
return c.req.Method return c.Req.Method
} }
// PathMatches returns true if the path portion of the request // PathMatches returns true if the path portion of the request
// URL matches pattern. // URL matches pattern.
func (c context) PathMatches(pattern string) bool { func (c Context) PathMatches(pattern string) bool {
return middleware.Path(c.req.URL.Path).Matches(pattern) return Path(c.Req.URL.Path).Matches(pattern)
} }
...@@ -119,7 +119,12 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error ...@@ -119,7 +119,12 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
return http.StatusInternalServerError, err 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 { if err != nil {
return http.StatusInternalServerError, err 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 ( ...@@ -9,6 +9,7 @@ import (
"strings" "strings"
"text/template" "text/template"
"github.com/mholt/caddy/middleware"
"github.com/russross/blackfriday" "github.com/russross/blackfriday"
) )
...@@ -17,9 +18,14 @@ const ( ...@@ -17,9 +18,14 @@ const (
DefaultStaticDir = "generated_site" 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 // Process processes the contents of a page in b. It parses the metadata
// (if any) and uses the template (if found). // (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 metadata = Metadata{Variables: make(map[string]interface{})}
var markdown []byte var markdown []byte
var err error var err error
...@@ -61,14 +67,21 @@ func (md Markdown) Process(c Config, requestPath string, b []byte) ([]byte, erro ...@@ -61,14 +67,21 @@ func (md Markdown) Process(c Config, requestPath string, b []byte) ([]byte, erro
markdown = blackfriday.Markdown(markdown, c.Renderer, 0) markdown = blackfriday.Markdown(markdown, c.Renderer, 0)
// set it as body for template // 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, // processTemplate processes a template given a requestPath,
// template (tmpl) and metadata // 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, // if template is not specified,
// use the default template // use the default template
if tmpl == nil { if tmpl == nil {
...@@ -81,7 +94,12 @@ func (md Markdown) processTemplate(c Config, requestPath string, tmpl []byte, me ...@@ -81,7 +94,12 @@ func (md Markdown) processTemplate(c Config, requestPath string, tmpl []byte, me
if err != nil { if err != nil {
return nil, err 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 return nil, err
} }
...@@ -148,15 +166,7 @@ func defaultTemplate(c Config, metadata Metadata, requestPath string) []byte { ...@@ -148,15 +166,7 @@ func defaultTemplate(c Config, metadata Metadata, requestPath string) []byte {
} }
// Title is first line (length-limited), otherwise filename // Title is first line (length-limited), otherwise filename
title := metadata.Title title, _ := metadata.Variables["title"].(string)
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)
}
}
html := []byte(htmlTemplate) html := []byte(htmlTemplate)
html = bytes.Replace(html, []byte("{{title}}"), []byte(title), 1) html = bytes.Replace(html, []byte("{{title}}"), []byte(title), 1)
...@@ -176,7 +186,7 @@ const ( ...@@ -176,7 +186,7 @@ const (
{{js}} {{js}}
</head> </head>
<body> <body>
{{.markdown}} {{.Doc.body}}
</body> </body>
</html>` </html>`
cssTemplate = `<link rel="stylesheet" href="{{url}}">` 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 ...@@ -31,7 +31,7 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
for _, ext := range rule.Extensions { for _, ext := range rule.Extensions {
if reqExt == ext { if reqExt == ext {
// Create execution context // 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 // Build the template
tpl, err := template.ParseFiles(filepath.Join(t.Root, fpath)) 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