-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathpage.go
199 lines (182 loc) · 6.07 KB
/
page.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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package main
import (
"bytes"
"github.com/microcosm-cc/bluemonday"
"html/template"
"log"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"time"
)
// Page is a struct containing information about a single page. Title is the title extracted from the page content using
// titleRegexp. Name is the path without extension (so a path of "foo.md" results in the Name "foo"). Body is the
// Markdown content of the page and Html is the rendered HTML for that Markdown.
type Page struct {
Title string
Name string
Body []byte
Html template.HTML
Hashtags []string
}
// Link is a struct containing a title and a name. Name is the path without extension (so a path of "foo.md" results in
// the Name "foo").
type Link struct {
Title string
Url string
}
// blogRe is a regular expression that matches blog pages. If the filename of a blog page starts with an ISO date
// (YYYY-MM-DD), then it's a blog page.
var blogRe = regexp.MustCompile(`^\d\d\d\d-\d\d-\d\d`)
// santizeStrict uses bluemonday to sanitize the HTML away. No elements are allowed except for the b tag because this is
// used for snippets.
func sanitizeStrict(s string) template.HTML {
policy := bluemonday.StrictPolicy()
policy.AllowElements("b")
return template.HTML(policy.Sanitize(s))
}
// unsafeBytes does not use bluemonday to sanitize the HTML used for pages. This is where you make changes if you want
// to be more lenient. If you look at the git repository, there are older versions containing the function sanitizeBytes
// which would do elaborate checking.
func unsafeBytes(bytes []byte) template.HTML {
return template.HTML(bytes)
}
// nameEscape returns the page name safe for use in URLs. That is, percent escaping is used except for the slashes.
func nameEscape(s string) string {
parts := strings.Split(s, "/")
for i, part := range parts {
parts[i] = url.PathEscape(part)
}
return strings.Join(parts, "/")
}
// save saves a Page. The path is based on the Page.Name and gets the ".md" extension. Page.Body is saved, without any
// carriage return characters ("\r"). Page.Title and Page.Html are not saved. There is no caching. Before removing or
// writing a file, the old copy is renamed to a backup, appending "~". Errors are not logged but returned.
func (p *Page) save() error {
fp := filepath.FromSlash(p.Name + ".md")
watches.ignore(fp)
s := bytes.ReplaceAll(p.Body, []byte{'\r'}, []byte{})
if len(s) == 0 {
log.Println("Delete", p.Name)
index.remove(p)
return os.Rename(fp, fp+"~")
}
p.Body = s
index.update(p)
d := filepath.Dir(fp)
if d != "." {
err := os.MkdirAll(d, 0755)
if err != nil {
return err
}
}
err := backup(fp)
if err != nil {
return err
}
return os.WriteFile(fp, s, 0644)
}
// backup a file by renaming it unless the existing backup is less than an hour old. A backup gets a tilde appended to
// it ("~"). This is true even if the file refers to a binary file like "image.png" and most applications don't know
// what to do with a file called "image.png~". This expects a file path. Use filepath.FromSlash(path) if necessary. The
// backup file gets its modification time set to now so that subsequent edits don't immediately overwrite it again.
func backup(fp string) error {
_, err := os.Stat(fp)
if err != nil {
return nil
}
bp := fp + "~"
fi, err := os.Stat(bp)
if err != nil || time.Since(fi.ModTime()).Minutes() >= 60 {
err = os.Rename(fp, bp)
if err != nil {
return err
}
ts := time.Now()
return os.Chtimes(bp, ts, ts)
}
return nil
}
// loadPage loads a Page given a name. The path loaded is that Page.Name with the ".md" extension. The Page.Title is set
// to the Page.Name (and possibly changed, later). The Page.Body is set to the file content. The Page.Html remains
// undefined (there is no caching).
func loadPage(path string) (*Page, error) {
path = strings.TrimPrefix(path, "./") // result of a filepath.TreeWalk starting with "."
body, err := os.ReadFile(filepath.FromSlash(path + ".md"))
if err != nil {
return nil, err
}
return &Page{Title: path, Name: path, Body: body}, nil
}
// handleTitle extracts the title from a Page and sets Page.Title, if any. If replace is true, the page title is also
// removed from Page.Body. Make sure not to save this! This is only for rendering. In a template, the title is a
// separate attribute and is not repeated in the HTML.
func (p *Page) handleTitle(replace bool) {
s := string(p.Body)
m := titleRegexp.FindStringSubmatch(s)
if m != nil {
p.Title = m[1]
if replace {
p.Body = []byte(strings.Replace(s, m[0], "", 1))
}
}
}
// summarize sets Page.Html to an extract.
func (p *Page) summarize(q string) {
t := p.plainText()
p.Name = nameEscape(p.Name)
p.Html = sanitizeStrict(snippets(q, t))
}
// IsBlog returns true if the page name starts with an ISO date
func (p *Page) IsBlog() bool {
name := path.Base(p.Name)
return blogRe.MatchString(name)
}
// Dir returns the directory the page is in. It's either the empty string if the page is in the Oddmu working directory,
// or it ends in a slash. This is used to create the upload link in "view.html", for example.
func (p *Page) Dir() string {
d := filepath.Dir(p.Name)
if d == "." {
return ""
}
return d + "/"
}
// Base returns the basename of the page name: no directory. This is used to create the upload link in "view.html", for
// example.
func (p *Page) Base() string {
n := filepath.Base(p.Name)
if n == "." {
return ""
}
return n
}
// Today returns the date, as a string, for use in templates.
func (p *Page) Today() string {
return time.Now().Format(time.DateOnly)
}
// Parents returns a Link array to parent pages, up the directory structure.
func (p *Page) Parents() []*Link {
links := make([]*Link, 0)
index.RLock()
defer index.RUnlock()
// foo/bar/baz ⇒ index, foo/index
elems := strings.Split(p.Name, "/")
if len(elems) == 1 {
return links
}
s := ""
for i := 0; i < len(elems)-1; i++ {
name := s + "index"
title, ok := index.titles[name]
if !ok {
title = "…"
}
link := &Link{Title: title, Url: strings.Repeat("../", len(elems)-i-1) + "index"}
links = append(links, link)
s += elems[i] + "/"
}
return links
}