|  | // Copyright 2013 The Go Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style | 
|  | // license that can be found in the LICENSE file. | 
|  |  | 
|  | // Package blog implements a web server for articles written in present format. | 
|  | package blog // import "golang.org/x/tools/blog" | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "encoding/json" | 
|  | "encoding/xml" | 
|  | "fmt" | 
|  | "html/template" | 
|  | "log" | 
|  | "net/http" | 
|  | "os" | 
|  | "path/filepath" | 
|  | "regexp" | 
|  | "sort" | 
|  | "strings" | 
|  | "time" | 
|  |  | 
|  | "golang.org/x/tools/blog/atom" | 
|  | "golang.org/x/tools/present" | 
|  | ) | 
|  |  | 
|  | var ( | 
|  | validJSONPFunc = regexp.MustCompile(`(?i)^[a-z_][a-z0-9_.]*$`) | 
|  | // used to serve relative paths when ServeLocalLinks is enabled. | 
|  | golangOrgAbsLinkReplacer = strings.NewReplacer( | 
|  | `href="https://golang.org/pkg`, `href="/pkg`, | 
|  | `href="https://golang.org/cmd`, `href="/cmd`, | 
|  | ) | 
|  | ) | 
|  |  | 
|  | // Config specifies Server configuration values. | 
|  | type Config struct { | 
|  | ContentPath  string // Relative or absolute location of article files and related content. | 
|  | TemplatePath string // Relative or absolute location of template files. | 
|  |  | 
|  | BaseURL       string        // Absolute base URL (for permalinks; no trailing slash). | 
|  | BasePath      string        // Base URL path relative to server root (no trailing slash). | 
|  | GodocURL      string        // The base URL of godoc (for menu bar; no trailing slash). | 
|  | Hostname      string        // Server host name, used for rendering ATOM feeds. | 
|  | AnalyticsHTML template.HTML // Optional analytics HTML to insert at the beginning of <head>. | 
|  |  | 
|  | HomeArticles int    // Articles to display on the home page. | 
|  | FeedArticles int    // Articles to include in Atom and JSON feeds. | 
|  | FeedTitle    string // The title of the Atom XML feed | 
|  |  | 
|  | PlayEnabled     bool | 
|  | ServeLocalLinks bool // rewrite golang.org/{pkg,cmd} links to host-less, relative paths. | 
|  | } | 
|  |  | 
|  | // Doc represents an article adorned with presentation data. | 
|  | type Doc struct { | 
|  | *present.Doc | 
|  | Permalink string        // Canonical URL for this document. | 
|  | Path      string        // Path relative to server root (including base). | 
|  | HTML      template.HTML // rendered article | 
|  |  | 
|  | Related      []*Doc | 
|  | Newer, Older *Doc | 
|  | } | 
|  |  | 
|  | // Server implements an http.Handler that serves blog articles. | 
|  | type Server struct { | 
|  | cfg       Config | 
|  | docs      []*Doc | 
|  | redirects map[string]string | 
|  | tags      []string | 
|  | docPaths  map[string]*Doc // key is path without BasePath. | 
|  | docTags   map[string][]*Doc | 
|  | template  struct { | 
|  | home, index, article, doc *template.Template | 
|  | } | 
|  | atomFeed []byte // pre-rendered Atom feed | 
|  | jsonFeed []byte // pre-rendered JSON feed | 
|  | content  http.Handler | 
|  | } | 
|  |  | 
|  | // NewServer constructs a new Server using the specified config. | 
|  | func NewServer(cfg Config) (*Server, error) { | 
|  | present.PlayEnabled = cfg.PlayEnabled | 
|  |  | 
|  | if notExist(cfg.TemplatePath) { | 
|  | return nil, fmt.Errorf("template directory not found: %s", cfg.TemplatePath) | 
|  | } | 
|  | root := filepath.Join(cfg.TemplatePath, "root.tmpl") | 
|  | parse := func(name string) (*template.Template, error) { | 
|  | path := filepath.Join(cfg.TemplatePath, name) | 
|  | if notExist(path) { | 
|  | return nil, fmt.Errorf("template %s was not found in %s", name, cfg.TemplatePath) | 
|  | } | 
|  | t := template.New("").Funcs(funcMap) | 
|  | return t.ParseFiles(root, path) | 
|  | } | 
|  |  | 
|  | s := &Server{cfg: cfg} | 
|  |  | 
|  | // Parse templates. | 
|  | var err error | 
|  | s.template.home, err = parse("home.tmpl") | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | s.template.index, err = parse("index.tmpl") | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | s.template.article, err = parse("article.tmpl") | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | p := present.Template().Funcs(funcMap) | 
|  | s.template.doc, err = p.ParseFiles(filepath.Join(cfg.TemplatePath, "doc.tmpl")) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  |  | 
|  | // Load content. | 
|  | content := filepath.Clean(cfg.ContentPath) | 
|  | err = s.loadDocs(content) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  |  | 
|  | err = s.renderAtomFeed() | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  |  | 
|  | err = s.renderJSONFeed() | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  |  | 
|  | // Set up content file server. | 
|  | s.content = http.StripPrefix(s.cfg.BasePath, http.FileServer(http.Dir(cfg.ContentPath))) | 
|  |  | 
|  | return s, nil | 
|  | } | 
|  |  | 
|  | var funcMap = template.FuncMap{ | 
|  | "sectioned": sectioned, | 
|  | "authors":   authors, | 
|  | } | 
|  |  | 
|  | // sectioned returns true if the provided Doc contains more than one section. | 
|  | // This is used to control whether to display the table of contents and headings. | 
|  | func sectioned(d *present.Doc) bool { | 
|  | return len(d.Sections) > 1 | 
|  | } | 
|  |  | 
|  | // authors returns a comma-separated list of author names. | 
|  | func authors(authors []present.Author) string { | 
|  | var b bytes.Buffer | 
|  | last := len(authors) - 1 | 
|  | for i, a := range authors { | 
|  | if i > 0 { | 
|  | if i == last { | 
|  | if len(authors) > 2 { | 
|  | b.WriteString(",") | 
|  | } | 
|  | b.WriteString(" and ") | 
|  | } else { | 
|  | b.WriteString(", ") | 
|  | } | 
|  | } | 
|  | b.WriteString(authorName(a)) | 
|  | } | 
|  | return b.String() | 
|  | } | 
|  |  | 
|  | // authorName returns the first line of the Author text: the author's name. | 
|  | func authorName(a present.Author) string { | 
|  | el := a.TextElem() | 
|  | if len(el) == 0 { | 
|  | return "" | 
|  | } | 
|  | text, ok := el[0].(present.Text) | 
|  | if !ok || len(text.Lines) == 0 { | 
|  | return "" | 
|  | } | 
|  | return text.Lines[0] | 
|  | } | 
|  |  | 
|  | // loadDocs reads all content from the provided file system root, renders all | 
|  | // the articles it finds, adds them to the Server's docs field, computes the | 
|  | // denormalized docPaths, docTags, and tags fields, and populates the various | 
|  | // helper fields (Next, Previous, Related) for each Doc. | 
|  | func (s *Server) loadDocs(root string) error { | 
|  | // Read content into docs field. | 
|  | const ext = ".article" | 
|  | fn := func(p string, info os.FileInfo, err error) error { | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  |  | 
|  | if filepath.Ext(p) != ext { | 
|  | return nil | 
|  | } | 
|  | f, err := os.Open(p) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | defer f.Close() | 
|  | d, err := present.Parse(f, p, 0) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | var html bytes.Buffer | 
|  | err = d.Render(&html, s.template.doc) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | p = p[len(root) : len(p)-len(ext)] // trim root and extension | 
|  | p = filepath.ToSlash(p) | 
|  | s.docs = append(s.docs, &Doc{ | 
|  | Doc:       d, | 
|  | Path:      s.cfg.BasePath + p, | 
|  | Permalink: s.cfg.BaseURL + p, | 
|  | HTML:      template.HTML(html.String()), | 
|  | }) | 
|  | return nil | 
|  | } | 
|  | err := filepath.Walk(root, fn) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | sort.Sort(docsByTime(s.docs)) | 
|  |  | 
|  | // Pull out doc paths and tags and put in reverse-associating maps. | 
|  | s.docPaths = make(map[string]*Doc) | 
|  | s.docTags = make(map[string][]*Doc) | 
|  | s.redirects = make(map[string]string) | 
|  | for _, d := range s.docs { | 
|  | s.docPaths[strings.TrimPrefix(d.Path, s.cfg.BasePath)] = d | 
|  | for _, t := range d.Tags { | 
|  | s.docTags[t] = append(s.docTags[t], d) | 
|  | } | 
|  | } | 
|  | for _, d := range s.docs { | 
|  | for _, old := range d.OldURL { | 
|  | if !strings.HasPrefix(old, "/") { | 
|  | old = "/" + old | 
|  | } | 
|  | if _, ok := s.docPaths[old]; ok { | 
|  | return fmt.Errorf("redirect %s -> %s conflicts with document %s", old, d.Path, old) | 
|  | } | 
|  | if new, ok := s.redirects[old]; ok { | 
|  | return fmt.Errorf("redirect %s -> %s conflicts with redirect %s -> %s", old, d.Path, old, new) | 
|  | } | 
|  | s.redirects[old] = d.Path | 
|  | } | 
|  | } | 
|  |  | 
|  | // Pull out unique sorted list of tags. | 
|  | for t := range s.docTags { | 
|  | s.tags = append(s.tags, t) | 
|  | } | 
|  | sort.Strings(s.tags) | 
|  |  | 
|  | // Set up presentation-related fields, Newer, Older, and Related. | 
|  | for _, doc := range s.docs { | 
|  | // Newer, Older: docs adjacent to doc | 
|  | for i := range s.docs { | 
|  | if s.docs[i] != doc { | 
|  | continue | 
|  | } | 
|  | if i > 0 { | 
|  | doc.Newer = s.docs[i-1] | 
|  | } | 
|  | if i+1 < len(s.docs) { | 
|  | doc.Older = s.docs[i+1] | 
|  | } | 
|  | break | 
|  | } | 
|  |  | 
|  | // Related: all docs that share tags with doc. | 
|  | related := make(map[*Doc]bool) | 
|  | for _, t := range doc.Tags { | 
|  | for _, d := range s.docTags[t] { | 
|  | if d != doc { | 
|  | related[d] = true | 
|  | } | 
|  | } | 
|  | } | 
|  | for d := range related { | 
|  | doc.Related = append(doc.Related, d) | 
|  | } | 
|  | sort.Sort(docsByTime(doc.Related)) | 
|  | } | 
|  |  | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // renderAtomFeed generates an XML Atom feed and stores it in the Server's | 
|  | // atomFeed field. | 
|  | func (s *Server) renderAtomFeed() error { | 
|  | var updated time.Time | 
|  | if len(s.docs) > 0 { | 
|  | updated = s.docs[0].Time | 
|  | } | 
|  | feed := atom.Feed{ | 
|  | Title:   s.cfg.FeedTitle, | 
|  | ID:      "tag:" + s.cfg.Hostname + ",2013:" + s.cfg.Hostname, | 
|  | Updated: atom.Time(updated), | 
|  | Link: []atom.Link{{ | 
|  | Rel:  "self", | 
|  | Href: s.cfg.BaseURL + "/feed.atom", | 
|  | }}, | 
|  | } | 
|  | for i, doc := range s.docs { | 
|  | if i >= s.cfg.FeedArticles { | 
|  | break | 
|  | } | 
|  |  | 
|  | // Use original article path as ID in atom feed | 
|  | // to avoid articles being treated as new when renamed. | 
|  | idPath := doc.Path | 
|  | if len(doc.OldURL) > 0 { | 
|  | old := doc.OldURL[0] | 
|  | if !strings.HasPrefix(old, "/") { | 
|  | old = "/" + old | 
|  | } | 
|  | idPath = old | 
|  | } | 
|  |  | 
|  | e := &atom.Entry{ | 
|  | Title: doc.Title, | 
|  | ID:    feed.ID + idPath, | 
|  | Link: []atom.Link{{ | 
|  | Rel:  "alternate", | 
|  | Href: doc.Permalink, | 
|  | }}, | 
|  | Published: atom.Time(doc.Time), | 
|  | Updated:   atom.Time(doc.Time), | 
|  | Summary: &atom.Text{ | 
|  | Type: "html", | 
|  | Body: summary(doc), | 
|  | }, | 
|  | Content: &atom.Text{ | 
|  | Type: "html", | 
|  | Body: string(doc.HTML), | 
|  | }, | 
|  | Author: &atom.Person{ | 
|  | Name: authors(doc.Authors), | 
|  | }, | 
|  | } | 
|  | feed.Entry = append(feed.Entry, e) | 
|  | } | 
|  | data, err := xml.Marshal(&feed) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | s.atomFeed = data | 
|  | return nil | 
|  | } | 
|  |  | 
|  | type jsonItem struct { | 
|  | Title   string | 
|  | Link    string | 
|  | Time    time.Time | 
|  | Summary string | 
|  | Content string | 
|  | Author  string | 
|  | } | 
|  |  | 
|  | // renderJSONFeed generates a JSON feed and stores it in the Server's jsonFeed | 
|  | // field. | 
|  | func (s *Server) renderJSONFeed() error { | 
|  | var feed []jsonItem | 
|  | for i, doc := range s.docs { | 
|  | if i >= s.cfg.FeedArticles { | 
|  | break | 
|  | } | 
|  | item := jsonItem{ | 
|  | Title:   doc.Title, | 
|  | Link:    doc.Permalink, | 
|  | Time:    doc.Time, | 
|  | Summary: summary(doc), | 
|  | Content: string(doc.HTML), | 
|  | Author:  authors(doc.Authors), | 
|  | } | 
|  | feed = append(feed, item) | 
|  | } | 
|  | data, err := json.Marshal(feed) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | s.jsonFeed = data | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // summary returns the first paragraph of text from the provided Doc. | 
|  | func summary(d *Doc) string { | 
|  | if len(d.Sections) == 0 { | 
|  | return "" | 
|  | } | 
|  | for _, elem := range d.Sections[0].Elem { | 
|  | text, ok := elem.(present.Text) | 
|  | if !ok || text.Pre { | 
|  | // skip everything but non-text elements | 
|  | continue | 
|  | } | 
|  | var buf bytes.Buffer | 
|  | for _, s := range text.Lines { | 
|  | buf.WriteString(string(present.Style(s))) | 
|  | buf.WriteByte('\n') | 
|  | } | 
|  | return buf.String() | 
|  | } | 
|  | return "" | 
|  | } | 
|  |  | 
|  | // rootData encapsulates data destined for the root template. | 
|  | type rootData struct { | 
|  | Doc           *Doc | 
|  | BasePath      string | 
|  | GodocURL      string | 
|  | AnalyticsHTML template.HTML | 
|  | Data          interface{} | 
|  | } | 
|  |  | 
|  | // ServeHTTP serves the front, index, and article pages | 
|  | // as well as the ATOM and JSON feeds. | 
|  | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | 
|  | var ( | 
|  | d = rootData{ | 
|  | BasePath:      s.cfg.BasePath, | 
|  | GodocURL:      s.cfg.GodocURL, | 
|  | AnalyticsHTML: s.cfg.AnalyticsHTML, | 
|  | } | 
|  | t *template.Template | 
|  | ) | 
|  | switch p := strings.TrimPrefix(r.URL.Path, s.cfg.BasePath); p { | 
|  | case "/": | 
|  | d.Data = s.docs | 
|  | if len(s.docs) > s.cfg.HomeArticles { | 
|  | d.Data = s.docs[:s.cfg.HomeArticles] | 
|  | } | 
|  | t = s.template.home | 
|  | case "/index": | 
|  | d.Data = s.docs | 
|  | t = s.template.index | 
|  | case "/feed.atom", "/feeds/posts/default": | 
|  | w.Header().Set("Content-type", "application/atom+xml; charset=utf-8") | 
|  | w.Write(s.atomFeed) | 
|  | return | 
|  | case "/.json": | 
|  | if p := r.FormValue("jsonp"); validJSONPFunc.MatchString(p) { | 
|  | w.Header().Set("Content-type", "application/javascript; charset=utf-8") | 
|  | fmt.Fprintf(w, "%v(%s)", p, s.jsonFeed) | 
|  | return | 
|  | } | 
|  | w.Header().Set("Content-type", "application/json; charset=utf-8") | 
|  | w.Write(s.jsonFeed) | 
|  | return | 
|  | default: | 
|  | if redir, ok := s.redirects[p]; ok { | 
|  | http.Redirect(w, r, redir, http.StatusMovedPermanently) | 
|  | return | 
|  | } | 
|  | doc, ok := s.docPaths[p] | 
|  | if !ok { | 
|  | // Not a doc; try to just serve static content. | 
|  | s.content.ServeHTTP(w, r) | 
|  | return | 
|  | } | 
|  | d.Doc = doc | 
|  | t = s.template.article | 
|  | } | 
|  | var err error | 
|  | if s.cfg.ServeLocalLinks { | 
|  | var buf bytes.Buffer | 
|  | err = t.ExecuteTemplate(&buf, "root", d) | 
|  | if err != nil { | 
|  | log.Println(err) | 
|  | return | 
|  | } | 
|  | _, err = golangOrgAbsLinkReplacer.WriteString(w, buf.String()) | 
|  | } else { | 
|  | err = t.ExecuteTemplate(w, "root", d) | 
|  | } | 
|  | if err != nil { | 
|  | log.Println(err) | 
|  | } | 
|  | } | 
|  |  | 
|  | // docsByTime implements sort.Interface, sorting Docs by their Time field. | 
|  | type docsByTime []*Doc | 
|  |  | 
|  | func (s docsByTime) Len() int           { return len(s) } | 
|  | func (s docsByTime) Swap(i, j int)      { s[i], s[j] = s[j], s[i] } | 
|  | func (s docsByTime) Less(i, j int) bool { return s[i].Time.After(s[j].Time) } | 
|  |  | 
|  | // notExist reports whether the path exists or not. | 
|  | func notExist(path string) bool { | 
|  | _, err := os.Stat(path) | 
|  | return os.IsNotExist(err) | 
|  | } |