GoPLS Viewer

Home|gopls/blog/blog.go
1// Copyright 2013 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package blog implements a web server for articles written in present format.
6package blog // import "golang.org/x/tools/blog"
7
8import (
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
27var (
28    validJSONPFunc = regexp.MustCompile(`(?i)^[a-z_][a-z0-9_.]*$`)
29    // used to serve relative paths when ServeLocalLinks is enabled.
30    golangOrgAbsLinkReplacer = strings.NewReplacer(
31        `href="https://golang.org/pkg``href="/pkg`,
32        `href="https://golang.org/cmd``href="/cmd`,
33    )
34)
35
36// Config specifies Server configuration values.
37type Config struct {
38    ContentPath  string // Relative or absolute location of article files and related content.
39    TemplatePath string // Relative or absolute location of template files.
40
41    BaseURL       string        // Absolute base URL (for permalinks; no trailing slash).
42    BasePath      string        // Base URL path relative to server root (no trailing slash).
43    GodocURL      string        // The base URL of godoc (for menu bar; no trailing slash).
44    Hostname      string        // Server host name, used for rendering ATOM feeds.
45    AnalyticsHTML template.HTML // Optional analytics HTML to insert at the beginning of <head>.
46
47    HomeArticles int    // Articles to display on the home page.
48    FeedArticles int    // Articles to include in Atom and JSON feeds.
49    FeedTitle    string // The title of the Atom XML feed
50
51    PlayEnabled     bool
52    ServeLocalLinks bool // rewrite golang.org/{pkg,cmd} links to host-less, relative paths.
53}
54
55// Doc represents an article adorned with presentation data.
56type Doc struct {
57    *present.Doc
58    Permalink string        // Canonical URL for this document.
59    Path      string        // Path relative to server root (including base).
60    HTML      template.HTML // rendered article
61
62    Related      []*Doc
63    NewerOlder *Doc
64}
65
66// Server implements an http.Handler that serves blog articles.
67type Server struct {
68    cfg       Config
69    docs      []*Doc
70    redirects map[string]string
71    tags      []string
72    docPaths  map[string]*Doc // key is path without BasePath.
73    docTags   map[string][]*Doc
74    template  struct {
75        homeindexarticledoc *template.Template
76    }
77    atomFeed []byte // pre-rendered Atom feed
78    jsonFeed []byte // pre-rendered JSON feed
79    content  http.Handler
80}
81
82// NewServer constructs a new Server using the specified config.
83func NewServer(cfg Config) (*Servererror) {
84    present.PlayEnabled = cfg.PlayEnabled
85
86    if notExist(cfg.TemplatePath) {
87        return nilfmt.Errorf("template directory not found: %s"cfg.TemplatePath)
88    }
89    root := filepath.Join(cfg.TemplatePath"root.tmpl")
90    parse := func(name string) (*template.Templateerror) {
91        path := filepath.Join(cfg.TemplatePathname)
92        if notExist(path) {
93            return nilfmt.Errorf("template %s was not found in %s"namecfg.TemplatePath)
94        }
95        t := template.New("").Funcs(funcMap)
96        return t.ParseFiles(rootpath)
97    }
98
99    s := &Server{cfgcfg}
100
101    // Parse templates.
102    var err error
103    s.template.homeerr = parse("home.tmpl")
104    if err != nil {
105        return nilerr
106    }
107    s.template.indexerr = parse("index.tmpl")
108    if err != nil {
109        return nilerr
110    }
111    s.template.articleerr = parse("article.tmpl")
112    if err != nil {
113        return nilerr
114    }
115    p := present.Template().Funcs(funcMap)
116    s.template.docerr = p.ParseFiles(filepath.Join(cfg.TemplatePath"doc.tmpl"))
117    if err != nil {
118        return nilerr
119    }
120
121    // Load content.
122    content := filepath.Clean(cfg.ContentPath)
123    err = s.loadDocs(content)
124    if err != nil {
125        return nilerr
126    }
127
128    err = s.renderAtomFeed()
129    if err != nil {
130        return nilerr
131    }
132
133    err = s.renderJSONFeed()
134    if err != nil {
135        return nilerr
136    }
137
138    // Set up content file server.
139    s.content = http.StripPrefix(s.cfg.BasePathhttp.FileServer(http.Dir(cfg.ContentPath)))
140
141    return snil
142}
143
144var funcMap = template.FuncMap{
145    "sectioned"sectioned,
146    "authors":   authors,
147}
148
149// sectioned returns true if the provided Doc contains more than one section.
150// This is used to control whether to display the table of contents and headings.
151func sectioned(d *present.Docbool {
152    return len(d.Sections) > 1
153}
154
155// authors returns a comma-separated list of author names.
156func authors(authors []present.Authorstring {
157    var b bytes.Buffer
158    last := len(authors) - 1
159    for ia := 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// authorName returns the first line of the Author text: the author's name.
176func authorName(a present.Authorstring {
177    el := a.TextElem()
178    if len(el) == 0 {
179        return ""
180    }
181    textok := el[0].(present.Text)
182    if !ok || len(text.Lines) == 0 {
183        return ""
184    }
185    return text.Lines[0]
186}
187
188// loadDocs reads all content from the provided file system root, renders all
189// the articles it finds, adds them to the Server's docs field, computes the
190// denormalized docPaths, docTags, and tags fields, and populates the various
191// helper fields (Next, Previous, Related) for each Doc.
192func (s *ServerloadDocs(root stringerror {
193    // Read content into docs field.
194    const ext = ".article"
195    fn := func(p stringinfo os.FileInfoerr errorerror {
196        if err != nil {
197            return err
198        }
199
200        if filepath.Ext(p) != ext {
201            return nil
202        }
203        ferr := os.Open(p)
204        if err != nil {
205            return err
206        }
207        defer f.Close()
208        derr := present.Parse(fp0)
209        if err != nil {
210            return err
211        }
212        var html bytes.Buffer
213        err = d.Render(&htmls.template.doc)
214        if err != nil {
215            return err
216        }
217        p = p[len(root) : len(p)-len(ext)] // trim root and extension
218        p = filepath.ToSlash(p)
219        s.docs = append(s.docs, &Doc{
220            Doc:       d,
221            Path:      s.cfg.BasePath + p,
222            Permalinks.cfg.BaseURL + p,
223            HTML:      template.HTML(html.String()),
224        })
225        return nil
226    }
227    err := filepath.Walk(rootfn)
228    if err != nil {
229        return err
230    }
231    sort.Sort(docsByTime(s.docs))
232
233    // Pull out doc paths and tags and put in reverse-associating maps.
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.Paths.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"oldd.Pathold)
250            }
251            if newok := s.redirects[old]; ok {
252                return fmt.Errorf("redirect %s -> %s conflicts with redirect %s -> %s"oldd.Patholdnew)
253            }
254            s.redirects[old] = d.Path
255        }
256    }
257
258    // Pull out unique sorted list of tags.
259    for t := range s.docTags {
260        s.tags = append(s.tagst)
261    }
262    sort.Strings(s.tags)
263
264    // Set up presentation-related fields, Newer, Older, and Related.
265    for _doc := range s.docs {
266        // Newer, Older: docs adjacent to doc
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        // Related: all docs that share tags with doc.
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.Relatedd)
291        }
292        sort.Sort(docsByTime(doc.Related))
293    }
294
295    return nil
296}
297
298// renderAtomFeed generates an XML Atom feed and stores it in the Server's
299// atomFeed field.
300func (s *ServerrenderAtomFeed() 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        Updatedatom.Time(updated),
309        Link: []atom.Link{{
310            Rel:  "self",
311            Hrefs.cfg.BaseURL + "/feed.atom",
312        }},
313    }
314    for idoc := range s.docs {
315        if i >= s.cfg.FeedArticles {
316            break
317        }
318
319        // Use original article path as ID in atom feed
320        // to avoid articles being treated as new when renamed.
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            Titledoc.Title,
332            ID:    feed.ID + idPath,
333            Link: []atom.Link{{
334                Rel:  "alternate",
335                Hrefdoc.Permalink,
336            }},
337            Publishedatom.Time(doc.Time),
338            Updated:   atom.Time(doc.Time),
339            Summary: &atom.Text{
340                Type"html",
341                Bodysummary(doc),
342            },
343            Content: &atom.Text{
344                Type"html",
345                Bodystring(doc.HTML),
346            },
347            Author: &atom.Person{
348                Nameauthors(doc.Authors),
349            },
350        }
351        feed.Entry = append(feed.Entrye)
352    }
353    dataerr := xml.Marshal(&feed)
354    if err != nil {
355        return err
356    }
357    s.atomFeed = data
358    return nil
359}
360
361type jsonItem struct {
362    Title   string
363    Link    string
364    Time    time.Time
365    Summary string
366    Content string
367    Author  string
368}
369
370// renderJSONFeed generates a JSON feed and stores it in the Server's jsonFeed
371// field.
372func (s *ServerrenderJSONFeed() error {
373    var feed []jsonItem
374    for idoc := 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            Summarysummary(doc),
383            Contentstring(doc.HTML),
384            Author:  authors(doc.Authors),
385        }
386        feed = append(feeditem)
387    }
388    dataerr := json.Marshal(feed)
389    if err != nil {
390        return err
391    }
392    s.jsonFeed = data
393    return nil
394}
395
396// summary returns the first paragraph of text from the provided Doc.
397func summary(d *Docstring {
398    if len(d.Sections) == 0 {
399        return ""
400    }
401    for _elem := range d.Sections[0].Elem {
402        textok := elem.(present.Text)
403        if !ok || text.Pre {
404            // skip everything but non-text elements
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// rootData encapsulates data destined for the root template.
418type rootData struct {
419    Doc           *Doc
420    BasePath      string
421    GodocURL      string
422    AnalyticsHTML template.HTML
423    Data          interface{}
424}
425
426// ServeHTTP serves the front, index, and article pages
427// as well as the ATOM and JSON feeds.
428func (s *ServerServeHTTP(w http.ResponseWriterr *http.Request) {
429    var (
430        d = rootData{
431            BasePath:      s.cfg.BasePath,
432            GodocURL:      s.cfg.GodocURL,
433            AnalyticsHTMLs.cfg.AnalyticsHTML,
434        }
435        t *template.Template
436    )
437    switch p := strings.TrimPrefix(r.URL.Paths.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)"ps.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 redirok := s.redirects[p]; ok {
462            http.Redirect(wrredirhttp.StatusMovedPermanently)
463            return
464        }
465        docok := s.docPaths[p]
466        if !ok {
467            // Not a doc; try to just serve static content.
468            s.content.ServeHTTP(wr)
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(wbuf.String())
483    } else {
484        err = t.ExecuteTemplate(w"root"d)
485    }
486    if err != nil {
487        log.Println(err)
488    }
489}
490
491// docsByTime implements sort.Interface, sorting Docs by their Time field.
492type docsByTime []*Doc
493
494func (s docsByTimeLen() int           { return len(s) }
495func (s docsByTimeSwap(ij int)      { s[i], s[j] = s[j], s[i] }
496func (s docsByTimeLess(ij intbool { return s[i].Time.After(s[j].Time) }
497
498// notExist reports whether the path exists or not.
499func notExist(path stringbool {
500    _err := os.Stat(path)
501    return os.IsNotExist(err)
502}
503
MembersX
rootData.Doc
Server.ServeHTTP.w
notExist
authors.b
Server.loadDocs.RangeStmt_7141.BlockStmt.RangeStmt_7483.BlockStmt.RangeStmt_7515.d
Server.loadDocs.RangeStmt_6991.t
Doc.Older
Server.atomFeed
summary.RangeStmt_9893.BlockStmt.buf
Doc
Doc.Newer
Config.PlayEnabled
Server.loadDocs.BlockStmt.html
rootData.AnalyticsHTML
Server.ServeHTTP.BlockStmt.buf
os
filepath
Server.loadDocs.RangeStmt_7141.BlockStmt.RangeStmt_7483.t
jsonItem
jsonItem.Time
Server.renderJSONFeed.s
NewServer
authors.RangeStmt_4488.a
docsByTime
Server.tags
Server.content
Server.template
notExist.err
Server.loadDocs.BlockStmt.err
Server.renderAtomFeed.err
rootData.GodocURL
NewServer.BlockStmt.path
Server.loadDocs.BlockStmt.f
Server.loadDocs.RangeStmt_6516.BlockStmt.RangeStmt_6545.old
Server.ServeHTTP.p
Config.Hostname
sectioned
summary
rootData.Data
xml
NewServer.BlockStmt.t
Config.BaseURL
Config.HomeArticles
Server
Server.renderAtomFeed.RangeStmt_8173.i
Server.renderJSONFeed.feed
Server.renderJSONFeed.RangeStmt_9360.i
bytes
regexp
docsByTime.Swap.j
docsByTime.Less.i
summary.d
docsByTime.Swap
Server.renderJSONFeed.err
rootData
docsByTime.Len.s
docsByTime.Less.s
present
NewServer.err
Server.docTags
NewServer.s
Server.renderAtomFeed
jsonItem.Author
json
Server.redirects
docsByTime.Swap.s
sectioned.d
authors
Server.loadDocs.root
Server.loadDocs.ext
Server.ServeHTTP.s
docsByTime.Len
fmt
Doc.Path
notExist._
NewServer.p
Server.loadDocs.err
Doc.Permalink
Doc.HTML
Doc.Related
Server.loadDocs.BlockStmt.d
Server.loadDocs.RangeStmt_6349.d
Server.loadDocs.RangeStmt_7141.BlockStmt.related
atom
Config.ServeLocalLinks
Server.renderAtomFeed.RangeStmt_8173.BlockStmt.idPath
jsonItem.Summary
Server.ServeHTTP.err
docsByTime.Less
notExist.path
Server.renderAtomFeed.s
Server.renderAtomFeed.RangeStmt_8173.doc
Server.renderAtomFeed.feed
jsonItem.Content
summary.RangeStmt_9893.BlockStmt.RangeStmt_10077.s
template
Config.AnalyticsHTML
Server.renderJSONFeed.data
Config
Server.loadDocs.RangeStmt_7141.BlockStmt.RangeStmt_7606.d
http
time
Config.BasePath
authorName.a
log
sort
jsonItem.Link
Server.renderJSONFeed.RangeStmt_9360.doc
Config.FeedTitle
authorName
Server.loadDocs.RangeStmt_7141.BlockStmt.RangeStmt_7212.i
Server.renderAtomFeed.updated
Server.renderJSONFeed
Server.renderJSONFeed.RangeStmt_9360.BlockStmt.item
Server.ServeHTTP.r
docsByTime.Less.j
NewServer.root
Server.loadDocs.RangeStmt_6349.BlockStmt.RangeStmt_6439.t
Server.loadDocs.s
strings
docsByTime.Swap.i
Server.jsonFeed
Server.loadDocs
Server.loadDocs.RangeStmt_7141.doc
Server.renderAtomFeed.RangeStmt_8173.BlockStmt.e
Server.ServeHTTP.BlockStmt.p
Config.GodocURL
Server.docPaths
Config.FeedArticles
authors.RangeStmt_4488.i
summary.RangeStmt_9893.elem
rootData.BasePath
Server.ServeHTTP
Server.ServeHTTP.t
Config.ContentPath
Config.TemplatePath
authors.authors
jsonItem.Title
NewServer.cfg
NewServer.content
Server.docs
Server.renderAtomFeed.data
Server.loadDocs.RangeStmt_6516.d
Server.cfg
authorName.el
Members
X