1
2
3
4
5
6 package blog
7
8 import (
9 "bytes"
10 "encoding/json"
11 "encoding/xml"
12 "fmt"
13 "html/template"
14 "log"
15 "net/http"
16 "os"
17 "path/filepath"
18 "regexp"
19 "sort"
20 "strings"
21 "time"
22
23 "golang.org/x/tools/blog/atom"
24 "golang.org/x/tools/present"
25 )
26
27 var (
28 validJSONPFunc = regexp.MustCompile(`(?i)^[a-z_][a-z0-9_.]*$`)
29
30 golangOrgAbsLinkReplacer = strings.NewReplacer(
31 `href="https://golang.org/pkg`, `href="/pkg`,
32 `href="https://golang.org/cmd`, `href="/cmd`,
33 )
34 )
35
36
37 type Config struct {
38 ContentPath string
39 TemplatePath string
40
41 BaseURL string
42 BasePath string
43 GodocURL string
44 Hostname string
45 AnalyticsHTML template.HTML
46
47 HomeArticles int
48 FeedArticles int
49 FeedTitle string
50
51 PlayEnabled bool
52 ServeLocalLinks bool
53 }
54
55
56 type Doc struct {
57 *present.Doc
58 Permalink string
59 Path string
60 HTML template.HTML
61
62 Related []*Doc
63 Newer, Older *Doc
64 }
65
66
67 type Server struct {
68 cfg Config
69 docs []*Doc
70 redirects map[string]string
71 tags []string
72 docPaths map[string]*Doc
73 docTags map[string][]*Doc
74 template struct {
75 home, index, article, doc *template.Template
76 }
77 atomFeed []byte
78 jsonFeed []byte
79 content http.Handler
80 }
81
82
83 func NewServer(cfg Config) (*Server, error) {
84 present.PlayEnabled = cfg.PlayEnabled
85
86 if notExist(cfg.TemplatePath) {
87 return nil, fmt.Errorf("template directory not found: %s", cfg.TemplatePath)
88 }
89 root := filepath.Join(cfg.TemplatePath, "root.tmpl")
90 parse := func(name string) (*template.Template, error) {
91 path := filepath.Join(cfg.TemplatePath, name)
92 if notExist(path) {
93 return nil, fmt.Errorf("template %s was not found in %s", name, cfg.TemplatePath)
94 }
95 t := template.New("").Funcs(funcMap)
96 return t.ParseFiles(root, path)
97 }
98
99 s := &Server{cfg: cfg}
100
101
102 var err error
103 s.template.home, err = parse("home.tmpl")
104 if err != nil {
105 return nil, err
106 }
107 s.template.index, err = parse("index.tmpl")
108 if err != nil {
109 return nil, err
110 }
111 s.template.article, err = parse("article.tmpl")
112 if err != nil {
113 return nil, err
114 }
115 p := present.Template().Funcs(funcMap)
116 s.template.doc, err = p.ParseFiles(filepath.Join(cfg.TemplatePath, "doc.tmpl"))
117 if err != nil {
118 return nil, err
119 }
120
121
122 content := filepath.Clean(cfg.ContentPath)
123 err = s.loadDocs(content)
124 if err != nil {
125 return nil, err
126 }
127
128 err = s.renderAtomFeed()
129 if err != nil {
130 return nil, err
131 }
132
133 err = s.renderJSONFeed()
134 if err != nil {
135 return nil, err
136 }
137
138
139 s.content = http.StripPrefix(s.cfg.BasePath, http.FileServer(http.Dir(cfg.ContentPath)))
140
141 return s, nil
142 }
143
144 var funcMap = template.FuncMap{
145 "sectioned": sectioned,
146 "authors": authors,
147 }
148
149
150
151 func sectioned(d *present.Doc) bool {
152 return len(d.Sections) > 1
153 }
154
155
156 func authors(authors []present.Author) string {
157 var b bytes.Buffer
158 last := len(authors) - 1
159 for i, a := range authors {
160 if i > 0 {
161 if i == last {
162 if len(authors) > 2 {
163 b.WriteString(",")
164 }
165 b.WriteString(" and ")
166 } else {
167 b.WriteString(", ")
168 }
169 }
170 b.WriteString(authorName(a))
171 }
172 return b.String()
173 }
174
175
176 func authorName(a present.Author) string {
177 el := a.TextElem()
178 if len(el) == 0 {
179 return ""
180 }
181 text, ok := el[0].(present.Text)
182 if !ok || len(text.Lines) == 0 {
183 return ""
184 }
185 return text.Lines[0]
186 }
187
188
189
190
191
192 func (s *Server) loadDocs(root string) error {
193
194 const ext = ".article"
195 fn := func(p string, info os.FileInfo, err error) error {
196 if err != nil {
197 return err
198 }
199
200 if filepath.Ext(p) != ext {
201 return nil
202 }
203 f, err := os.Open(p)
204 if err != nil {
205 return err
206 }
207 defer f.Close()
208 d, err := present.Parse(f, p, 0)
209 if err != nil {
210 return err
211 }
212 var html bytes.Buffer
213 err = d.Render(&html, s.template.doc)
214 if err != nil {
215 return err
216 }
217 p = p[len(root) : len(p)-len(ext)]
218 p = filepath.ToSlash(p)
219 s.docs = append(s.docs, &Doc{
220 Doc: d,
221 Path: s.cfg.BasePath + p,
222 Permalink: s.cfg.BaseURL + p,
223 HTML: template.HTML(html.String()),
224 })
225 return nil
226 }
227 err := filepath.Walk(root, fn)
228 if err != nil {
229 return err
230 }
231 sort.Sort(docsByTime(s.docs))
232
233
234 s.docPaths = make(map[string]*Doc)
235 s.docTags = make(map[string][]*Doc)
236 s.redirects = make(map[string]string)
237 for _, d := range s.docs {
238 s.docPaths[strings.TrimPrefix(d.Path, s.cfg.BasePath)] = d
239 for _, t := range d.Tags {
240 s.docTags[t] = append(s.docTags[t], d)
241 }
242 }
243 for _, d := range s.docs {
244 for _, old := range d.OldURL {
245 if !strings.HasPrefix(old, "/") {
246 old = "/" + old
247 }
248 if _, ok := s.docPaths[old]; ok {
249 return fmt.Errorf("redirect %s -> %s conflicts with document %s", old, d.Path, old)
250 }
251 if new, ok := s.redirects[old]; ok {
252 return fmt.Errorf("redirect %s -> %s conflicts with redirect %s -> %s", old, d.Path, old, new)
253 }
254 s.redirects[old] = d.Path
255 }
256 }
257
258
259 for t := range s.docTags {
260 s.tags = append(s.tags, t)
261 }
262 sort.Strings(s.tags)
263
264
265 for _, doc := range s.docs {
266
267 for i := range s.docs {
268 if s.docs[i] != doc {
269 continue
270 }
271 if i > 0 {
272 doc.Newer = s.docs[i-1]
273 }
274 if i+1 < len(s.docs) {
275 doc.Older = s.docs[i+1]
276 }
277 break
278 }
279
280
281 related := make(map[*Doc]bool)
282 for _, t := range doc.Tags {
283 for _, d := range s.docTags[t] {
284 if d != doc {
285 related[d] = true
286 }
287 }
288 }
289 for d := range related {
290 doc.Related = append(doc.Related, d)
291 }
292 sort.Sort(docsByTime(doc.Related))
293 }
294
295 return nil
296 }
297
298
299
300 func (s *Server) renderAtomFeed() error {
301 var updated time.Time
302 if len(s.docs) > 0 {
303 updated = s.docs[0].Time
304 }
305 feed := atom.Feed{
306 Title: s.cfg.FeedTitle,
307 ID: "tag:" + s.cfg.Hostname + ",2013:" + s.cfg.Hostname,
308 Updated: atom.Time(updated),
309 Link: []atom.Link{{
310 Rel: "self",
311 Href: s.cfg.BaseURL + "/feed.atom",
312 }},
313 }
314 for i, doc := range s.docs {
315 if i >= s.cfg.FeedArticles {
316 break
317 }
318
319
320
321 idPath := doc.Path
322 if len(doc.OldURL) > 0 {
323 old := doc.OldURL[0]
324 if !strings.HasPrefix(old, "/") {
325 old = "/" + old
326 }
327 idPath = old
328 }
329
330 e := &atom.Entry{
331 Title: doc.Title,
332 ID: feed.ID + idPath,
333 Link: []atom.Link{{
334 Rel: "alternate",
335 Href: doc.Permalink,
336 }},
337 Published: atom.Time(doc.Time),
338 Updated: atom.Time(doc.Time),
339 Summary: &atom.Text{
340 Type: "html",
341 Body: summary(doc),
342 },
343 Content: &atom.Text{
344 Type: "html",
345 Body: string(doc.HTML),
346 },
347 Author: &atom.Person{
348 Name: authors(doc.Authors),
349 },
350 }
351 feed.Entry = append(feed.Entry, e)
352 }
353 data, err := xml.Marshal(&feed)
354 if err != nil {
355 return err
356 }
357 s.atomFeed = data
358 return nil
359 }
360
361 type jsonItem struct {
362 Title string
363 Link string
364 Time time.Time
365 Summary string
366 Content string
367 Author string
368 }
369
370
371
372 func (s *Server) renderJSONFeed() error {
373 var feed []jsonItem
374 for i, doc := range s.docs {
375 if i >= s.cfg.FeedArticles {
376 break
377 }
378 item := jsonItem{
379 Title: doc.Title,
380 Link: doc.Permalink,
381 Time: doc.Time,
382 Summary: summary(doc),
383 Content: string(doc.HTML),
384 Author: authors(doc.Authors),
385 }
386 feed = append(feed, item)
387 }
388 data, err := json.Marshal(feed)
389 if err != nil {
390 return err
391 }
392 s.jsonFeed = data
393 return nil
394 }
395
396
397 func summary(d *Doc) string {
398 if len(d.Sections) == 0 {
399 return ""
400 }
401 for _, elem := range d.Sections[0].Elem {
402 text, ok := elem.(present.Text)
403 if !ok || text.Pre {
404
405 continue
406 }
407 var buf bytes.Buffer
408 for _, s := range text.Lines {
409 buf.WriteString(string(present.Style(s)))
410 buf.WriteByte('\n')
411 }
412 return buf.String()
413 }
414 return ""
415 }
416
417
418 type rootData struct {
419 Doc *Doc
420 BasePath string
421 GodocURL string
422 AnalyticsHTML template.HTML
423 Data interface{}
424 }
425
426
427
428 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
429 var (
430 d = rootData{
431 BasePath: s.cfg.BasePath,
432 GodocURL: s.cfg.GodocURL,
433 AnalyticsHTML: s.cfg.AnalyticsHTML,
434 }
435 t *template.Template
436 )
437 switch p := strings.TrimPrefix(r.URL.Path, s.cfg.BasePath); p {
438 case "/":
439 d.Data = s.docs
440 if len(s.docs) > s.cfg.HomeArticles {
441 d.Data = s.docs[:s.cfg.HomeArticles]
442 }
443 t = s.template.home
444 case "/index":
445 d.Data = s.docs
446 t = s.template.index
447 case "/feed.atom", "/feeds/posts/default":
448 w.Header().Set("Content-type", "application/atom+xml; charset=utf-8")
449 w.Write(s.atomFeed)
450 return
451 case "/.json":
452 if p := r.FormValue("jsonp"); validJSONPFunc.MatchString(p) {
453 w.Header().Set("Content-type", "application/javascript; charset=utf-8")
454 fmt.Fprintf(w, "%v(%s)", p, s.jsonFeed)
455 return
456 }
457 w.Header().Set("Content-type", "application/json; charset=utf-8")
458 w.Write(s.jsonFeed)
459 return
460 default:
461 if redir, ok := s.redirects[p]; ok {
462 http.Redirect(w, r, redir, http.StatusMovedPermanently)
463 return
464 }
465 doc, ok := s.docPaths[p]
466 if !ok {
467
468 s.content.ServeHTTP(w, r)
469 return
470 }
471 d.Doc = doc
472 t = s.template.article
473 }
474 var err error
475 if s.cfg.ServeLocalLinks {
476 var buf bytes.Buffer
477 err = t.ExecuteTemplate(&buf, "root", d)
478 if err != nil {
479 log.Println(err)
480 return
481 }
482 _, err = golangOrgAbsLinkReplacer.WriteString(w, buf.String())
483 } else {
484 err = t.ExecuteTemplate(w, "root", d)
485 }
486 if err != nil {
487 log.Println(err)
488 }
489 }
490
491
492 type docsByTime []*Doc
493
494 func (s docsByTime) Len() int { return len(s) }
495 func (s docsByTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
496 func (s docsByTime) Less(i, j int) bool { return s[i].Time.After(s[j].Time) }
497
498
499 func notExist(path string) bool {
500 _, err := os.Stat(path)
501 return os.IsNotExist(err)
502 }
503
View as plain text