markdown.go 3.84 KB
Newer Older
Matthew Holt's avatar
Matthew Holt committed
1 2 3 4 5 6 7
// Package markdown is middleware to render markdown files as HTML
// on-the-fly.
package markdown

import (
	"io/ioutil"
	"net/http"
8
	"os"
9
	"path"
10
	"text/template"
11
	"time"
Matthew Holt's avatar
Matthew Holt committed
12 13 14 15 16

	"github.com/mholt/caddy/middleware"
	"github.com/russross/blackfriday"
)

17 18
// Markdown implements a layer of middleware that serves
// markdown as HTML.
Matthew Holt's avatar
Matthew Holt committed
19 20 21 22
type Markdown struct {
	// Server root
	Root string

23 24 25
	// Jail the requests to site root with a mock file system
	FileSys http.FileSystem

Matthew Holt's avatar
Matthew Holt committed
26
	// Next HTTP handler in the chain
27
	Next middleware.Handler
Matthew Holt's avatar
Matthew Holt committed
28

29
	// The list of markdown configurations
30
	Configs []*Config
31 32 33

	// The list of index files to try
	IndexFiles []string
34 35
}

36 37
// Config stores markdown middleware configurations.
type Config struct {
Matthew Holt's avatar
Matthew Holt committed
38 39 40 41 42 43 44
	// Markdown renderer
	Renderer blackfriday.Renderer

	// Base path to match
	PathScope string

	// List of extensions to consider as markdown files
45
	Extensions map[string]struct{}
Matthew Holt's avatar
Matthew Holt committed
46 47 48 49 50 51

	// List of style sheets to load for each markdown file
	Styles []string

	// List of JavaScript files to load for each markdown file
	Scripts []string
52

53 54
	// Template(s) to render with
	Template *template.Template
55 56
}

Matthew Holt's avatar
Matthew Holt committed
57
// ServeHTTP implements the http.Handler interface.
58
func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
59 60 61 62 63
	var cfg *Config
	for _, c := range md.Configs {
		if middleware.Path(r.URL.Path).Matches(c.PathScope) { // not negated
			cfg = c
			break // or goto
64
		}
65 66 67 68
	}
	if cfg == nil {
		return md.Next.ServeHTTP(w, r) // exit early
	}
69

70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
	// We only deal with HEAD/GET
	switch r.Method {
	case http.MethodGet, http.MethodHead:
	default:
		return http.StatusMethodNotAllowed, nil
	}

	var dirents []os.FileInfo
	var lastModTime time.Time
	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 {
			if os.IsPermission(err) {
				return http.StatusForbidden, err
89
			}
90 91
			return http.StatusInternalServerError, err
		}
92

93 94 95 96 97 98 99
		// Grab a possible set of directory entries.  Note, we do not check
		// for errors here (unreadable directory, for example).  It may
		// still be useful to have a directory template file, without the
		// directory contents being present.
		dirents, _ = fdp.Readdir(-1)
		for _, d := range dirents {
			lastModTime = latest(lastModTime, d.ModTime())
100
		}
Matthew Holt's avatar
Matthew Holt committed
101

102 103 104
		// Set path to found index file
		fpath = idx
	}
105

106 107 108 109 110 111
	// If supported extension, process it
	if _, ok := cfg.Extensions[path.Ext(fpath)]; ok {
		f, err := md.FileSys.Open(fpath)
		if err != nil {
			if os.IsPermission(err) {
				return http.StatusForbidden, err
112
			}
113 114
			return http.StatusNotFound, nil
		}
115

116 117 118 119 120
		fs, err := f.Stat()
		if err != nil {
			return http.StatusNotFound, nil
		}
		lastModTime = latest(lastModTime, fs.ModTime())
121

122 123 124 125 126 127 128 129 130 131 132 133 134 135
		body, err := ioutil.ReadAll(f)
		if err != nil {
			return http.StatusInternalServerError, err
		}

		ctx := middleware.Context{
			Root: md.FileSys,
			Req:  r,
			URL:  r.URL,
		}
		html, err := cfg.Markdown(fpath, body, dirents, ctx)
		if err != nil {
			return http.StatusInternalServerError, err
		}
136

137 138 139 140 141 142
		// TODO(weingart): move template execution here, something like:
		//
		// html, err = md.execTemplate(cfg, html, ctx)
		// if err != nil {
		// 	return http.StatusInternalServerError, err
		// }
143

144 145
		middleware.SetLastModifiedHeader(w, lastModTime)
		if r.Method == "GET" {
146
			w.Write(html)
Matthew Holt's avatar
Matthew Holt committed
147
		}
148
		return http.StatusOK, nil
Matthew Holt's avatar
Matthew Holt committed
149 150
	}

151
	// Didn't qualify to serve as markdown; pass-thru
152
	return md.Next.ServeHTTP(w, r)
Matthew Holt's avatar
Matthew Holt committed
153
}
154 155 156 157 158 159 160 161 162 163 164 165 166

// latest returns the latest time.Time
func latest(t ...time.Time) time.Time {
	var last time.Time

	for _, tt := range t {
		if tt.After(last) {
			last = tt
		}
	}

	return last
}