forked from caddyserver/caddy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
markdown.go
171 lines (144 loc) · 4.16 KB
/
markdown.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// Package markdown is middleware to render markdown files as HTML
// on-the-fly.
package markdown
import (
"net/http"
"os"
"path"
"strconv"
"strings"
"text/template"
"time"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/russross/blackfriday"
)
// Markdown implements a layer of middleware that serves
// markdown as HTML.
type Markdown struct {
// Server root
Root string
// Jail the requests to site root with a mock file system
FileSys http.FileSystem
// Next HTTP handler in the chain
Next httpserver.Handler
// The list of markdown configurations
Configs []*Config
// The list of index files to try
IndexFiles []string
}
// Config stores markdown middleware configurations.
type Config struct {
// Markdown renderer
Renderer blackfriday.Renderer
// Base path to match
PathScope string
// List of extensions to consider as markdown files
Extensions map[string]struct{}
// List of style sheets to load for each markdown file
Styles []string
// List of JavaScript files to load for each markdown file
Scripts []string
// Template(s) to render with
Template *template.Template
}
// ServeHTTP implements the http.Handler interface.
func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
var cfg *Config
for _, c := range md.Configs {
if httpserver.Path(r.URL.Path).Matches(c.PathScope) { // not negated
cfg = c
break // or goto
}
}
if cfg == nil {
return md.Next.ServeHTTP(w, r) // exit early
}
// 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 := httpserver.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)
switch {
case err == nil: // nop
case os.IsPermission(err):
return http.StatusForbidden, err
case os.IsExist(err):
return http.StatusNotFound, nil
default: // did we run out of FD?
return http.StatusInternalServerError, err
}
defer fdp.Close()
// 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. Note, the directory's last
// modification is also present here (entry ".").
dirents, _ = fdp.Readdir(-1)
for _, d := range dirents {
lastModTime = latest(lastModTime, d.ModTime())
}
// Set path to found index file
fpath = idx
}
// If not supported extension, pass on it
if _, ok := cfg.Extensions[path.Ext(fpath)]; !ok {
return md.Next.ServeHTTP(w, r)
}
// At this point we have a supported extension/markdown
f, err := md.FileSys.Open(fpath)
switch {
case err == nil: // nop
case os.IsPermission(err):
return http.StatusForbidden, err
case os.IsExist(err):
return http.StatusNotFound, nil
default: // did we run out of FD?
return http.StatusInternalServerError, err
}
defer f.Close()
fs, err := f.Stat()
if err != nil {
return http.StatusGone, nil
}
lastModTime = latest(lastModTime, fs.ModTime())
ctx := httpserver.Context{
Root: md.FileSys,
Req: r,
URL: r.URL,
}
html, err := cfg.Markdown(title(fpath), f, dirents, ctx)
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Content-Length", strconv.FormatInt(int64(len(html)), 10))
httpserver.SetLastModifiedHeader(w, lastModTime)
if r.Method == http.MethodGet {
w.Write(html)
}
return http.StatusOK, nil
}
// 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
}
// title gives a backup generated title for a page
func title(p string) string {
return strings.TrimRight(path.Base(p), path.Ext(p))
}