github.com/jgarto/itcv@v0.0.0-20180826224514-4eea09c1aa0d/_vendor/src/golang.org/x/tools/blog/blog.go (about)

     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.
     6  package blog // import "golang.org/x/tools/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  	// 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.
    37  type 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  
    46  	HomeArticles int    // Articles to display on the home page.
    47  	FeedArticles int    // Articles to include in Atom and JSON feeds.
    48  	FeedTitle    string // The title of the Atom XML feed
    49  
    50  	PlayEnabled     bool
    51  	ServeLocalLinks bool // rewrite golang.org/{pkg,cmd} links to host-less, relative paths.
    52  }
    53  
    54  // Doc represents an article adorned with presentation data.
    55  type Doc struct {
    56  	*present.Doc
    57  	Permalink string        // Canonical URL for this document.
    58  	Path      string        // Path relative to server root (including base).
    59  	HTML      template.HTML // rendered article
    60  
    61  	Related      []*Doc
    62  	Newer, Older *Doc
    63  }
    64  
    65  // Server implements an http.Handler that serves blog articles.
    66  type Server struct {
    67  	cfg      Config
    68  	docs     []*Doc
    69  	tags     []string
    70  	docPaths map[string]*Doc // key is path without BasePath.
    71  	docTags  map[string][]*Doc
    72  	template struct {
    73  		home, index, article, doc *template.Template
    74  	}
    75  	atomFeed []byte // pre-rendered Atom feed
    76  	jsonFeed []byte // pre-rendered JSON feed
    77  	content  http.Handler
    78  }
    79  
    80  // NewServer constructs a new Server using the specified config.
    81  func NewServer(cfg Config) (*Server, error) {
    82  	present.PlayEnabled = cfg.PlayEnabled
    83  
    84  	if notExist(cfg.TemplatePath) {
    85  		return nil, fmt.Errorf("template directory not found: %s", cfg.TemplatePath)
    86  	}
    87  	root := filepath.Join(cfg.TemplatePath, "root.tmpl")
    88  	parse := func(name string) (*template.Template, error) {
    89  		path := filepath.Join(cfg.TemplatePath, name)
    90  		if notExist(path) {
    91  			return nil, fmt.Errorf("template %s was not found in %s", name, cfg.TemplatePath)
    92  		}
    93  		t := template.New("").Funcs(funcMap)
    94  		return t.ParseFiles(root, path)
    95  	}
    96  
    97  	s := &Server{cfg: cfg}
    98  
    99  	// Parse templates.
   100  	var err error
   101  	s.template.home, err = parse("home.tmpl")
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  	s.template.index, err = parse("index.tmpl")
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  	s.template.article, err = parse("article.tmpl")
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	p := present.Template().Funcs(funcMap)
   114  	s.template.doc, err = p.ParseFiles(filepath.Join(cfg.TemplatePath, "doc.tmpl"))
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	// Load content.
   120  	err = s.loadDocs(filepath.Clean(cfg.ContentPath))
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	err = s.renderAtomFeed()
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  
   130  	err = s.renderJSONFeed()
   131  	if err != nil {
   132  		return nil, err
   133  	}
   134  
   135  	// Set up content file server.
   136  	s.content = http.StripPrefix(s.cfg.BasePath, http.FileServer(http.Dir(cfg.ContentPath)))
   137  
   138  	return s, nil
   139  }
   140  
   141  var funcMap = template.FuncMap{
   142  	"sectioned": sectioned,
   143  	"authors":   authors,
   144  }
   145  
   146  // sectioned returns true if the provided Doc contains more than one section.
   147  // This is used to control whether to display the table of contents and headings.
   148  func sectioned(d *present.Doc) bool {
   149  	return len(d.Sections) > 1
   150  }
   151  
   152  // authors returns a comma-separated list of author names.
   153  func authors(authors []present.Author) string {
   154  	var b bytes.Buffer
   155  	last := len(authors) - 1
   156  	for i, a := range authors {
   157  		if i > 0 {
   158  			if i == last {
   159  				b.WriteString(" and ")
   160  			} else {
   161  				b.WriteString(", ")
   162  			}
   163  		}
   164  		b.WriteString(authorName(a))
   165  	}
   166  	return b.String()
   167  }
   168  
   169  // authorName returns the first line of the Author text: the author's name.
   170  func authorName(a present.Author) string {
   171  	el := a.TextElem()
   172  	if len(el) == 0 {
   173  		return ""
   174  	}
   175  	text, ok := el[0].(present.Text)
   176  	if !ok || len(text.Lines) == 0 {
   177  		return ""
   178  	}
   179  	return text.Lines[0]
   180  }
   181  
   182  // loadDocs reads all content from the provided file system root, renders all
   183  // the articles it finds, adds them to the Server's docs field, computes the
   184  // denormalized docPaths, docTags, and tags fields, and populates the various
   185  // helper fields (Next, Previous, Related) for each Doc.
   186  func (s *Server) loadDocs(root string) error {
   187  	// Read content into docs field.
   188  	const ext = ".article"
   189  	fn := func(p string, info os.FileInfo, err error) error {
   190  		if filepath.Ext(p) != ext {
   191  			return nil
   192  		}
   193  		f, err := os.Open(p)
   194  		if err != nil {
   195  			return err
   196  		}
   197  		defer f.Close()
   198  		d, err := present.Parse(f, p, 0)
   199  		if err != nil {
   200  			return err
   201  		}
   202  		var html bytes.Buffer
   203  		err = d.Render(&html, s.template.doc)
   204  		if err != nil {
   205  			return err
   206  		}
   207  		p = p[len(root) : len(p)-len(ext)] // trim root and extension
   208  		p = filepath.ToSlash(p)
   209  		s.docs = append(s.docs, &Doc{
   210  			Doc:       d,
   211  			Path:      s.cfg.BasePath + p,
   212  			Permalink: s.cfg.BaseURL + p,
   213  			HTML:      template.HTML(html.String()),
   214  		})
   215  		return nil
   216  	}
   217  	err := filepath.Walk(root, fn)
   218  	if err != nil {
   219  		return err
   220  	}
   221  	sort.Sort(docsByTime(s.docs))
   222  
   223  	// Pull out doc paths and tags and put in reverse-associating maps.
   224  	s.docPaths = make(map[string]*Doc)
   225  	s.docTags = make(map[string][]*Doc)
   226  	for _, d := range s.docs {
   227  		s.docPaths[strings.TrimPrefix(d.Path, s.cfg.BasePath)] = d
   228  		for _, t := range d.Tags {
   229  			s.docTags[t] = append(s.docTags[t], d)
   230  		}
   231  	}
   232  
   233  	// Pull out unique sorted list of tags.
   234  	for t := range s.docTags {
   235  		s.tags = append(s.tags, t)
   236  	}
   237  	sort.Strings(s.tags)
   238  
   239  	// Set up presentation-related fields, Newer, Older, and Related.
   240  	for _, doc := range s.docs {
   241  		// Newer, Older: docs adjacent to doc
   242  		for i := range s.docs {
   243  			if s.docs[i] != doc {
   244  				continue
   245  			}
   246  			if i > 0 {
   247  				doc.Newer = s.docs[i-1]
   248  			}
   249  			if i+1 < len(s.docs) {
   250  				doc.Older = s.docs[i+1]
   251  			}
   252  			break
   253  		}
   254  
   255  		// Related: all docs that share tags with doc.
   256  		related := make(map[*Doc]bool)
   257  		for _, t := range doc.Tags {
   258  			for _, d := range s.docTags[t] {
   259  				if d != doc {
   260  					related[d] = true
   261  				}
   262  			}
   263  		}
   264  		for d := range related {
   265  			doc.Related = append(doc.Related, d)
   266  		}
   267  		sort.Sort(docsByTime(doc.Related))
   268  	}
   269  
   270  	return nil
   271  }
   272  
   273  // renderAtomFeed generates an XML Atom feed and stores it in the Server's
   274  // atomFeed field.
   275  func (s *Server) renderAtomFeed() error {
   276  	var updated time.Time
   277  	if len(s.docs) > 0 {
   278  		updated = s.docs[0].Time
   279  	}
   280  	feed := atom.Feed{
   281  		Title:   s.cfg.FeedTitle,
   282  		ID:      "tag:" + s.cfg.Hostname + ",2013:" + s.cfg.Hostname,
   283  		Updated: atom.Time(updated),
   284  		Link: []atom.Link{{
   285  			Rel:  "self",
   286  			Href: s.cfg.BaseURL + "/feed.atom",
   287  		}},
   288  	}
   289  	for i, doc := range s.docs {
   290  		if i >= s.cfg.FeedArticles {
   291  			break
   292  		}
   293  		e := &atom.Entry{
   294  			Title: doc.Title,
   295  			ID:    feed.ID + doc.Path,
   296  			Link: []atom.Link{{
   297  				Rel:  "alternate",
   298  				Href: doc.Permalink,
   299  			}},
   300  			Published: atom.Time(doc.Time),
   301  			Updated:   atom.Time(doc.Time),
   302  			Summary: &atom.Text{
   303  				Type: "html",
   304  				Body: summary(doc),
   305  			},
   306  			Content: &atom.Text{
   307  				Type: "html",
   308  				Body: string(doc.HTML),
   309  			},
   310  			Author: &atom.Person{
   311  				Name: authors(doc.Authors),
   312  			},
   313  		}
   314  		feed.Entry = append(feed.Entry, e)
   315  	}
   316  	data, err := xml.Marshal(&feed)
   317  	if err != nil {
   318  		return err
   319  	}
   320  	s.atomFeed = data
   321  	return nil
   322  }
   323  
   324  type jsonItem struct {
   325  	Title   string
   326  	Link    string
   327  	Time    time.Time
   328  	Summary string
   329  	Content string
   330  	Author  string
   331  }
   332  
   333  // renderJSONFeed generates a JSON feed and stores it in the Server's jsonFeed
   334  // field.
   335  func (s *Server) renderJSONFeed() error {
   336  	var feed []jsonItem
   337  	for i, doc := range s.docs {
   338  		if i >= s.cfg.FeedArticles {
   339  			break
   340  		}
   341  		item := jsonItem{
   342  			Title:   doc.Title,
   343  			Link:    doc.Permalink,
   344  			Time:    doc.Time,
   345  			Summary: summary(doc),
   346  			Content: string(doc.HTML),
   347  			Author:  authors(doc.Authors),
   348  		}
   349  		feed = append(feed, item)
   350  	}
   351  	data, err := json.Marshal(feed)
   352  	if err != nil {
   353  		return err
   354  	}
   355  	s.jsonFeed = data
   356  	return nil
   357  }
   358  
   359  // summary returns the first paragraph of text from the provided Doc.
   360  func summary(d *Doc) string {
   361  	if len(d.Sections) == 0 {
   362  		return ""
   363  	}
   364  	for _, elem := range d.Sections[0].Elem {
   365  		text, ok := elem.(present.Text)
   366  		if !ok || text.Pre {
   367  			// skip everything but non-text elements
   368  			continue
   369  		}
   370  		var buf bytes.Buffer
   371  		for _, s := range text.Lines {
   372  			buf.WriteString(string(present.Style(s)))
   373  			buf.WriteByte('\n')
   374  		}
   375  		return buf.String()
   376  	}
   377  	return ""
   378  }
   379  
   380  // rootData encapsulates data destined for the root template.
   381  type rootData struct {
   382  	Doc      *Doc
   383  	BasePath string
   384  	GodocURL string
   385  	Data     interface{}
   386  }
   387  
   388  // ServeHTTP serves the front, index, and article pages
   389  // as well as the ATOM and JSON feeds.
   390  func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   391  	var (
   392  		d = rootData{BasePath: s.cfg.BasePath, GodocURL: s.cfg.GodocURL}
   393  		t *template.Template
   394  	)
   395  	switch p := strings.TrimPrefix(r.URL.Path, s.cfg.BasePath); p {
   396  	case "/":
   397  		d.Data = s.docs
   398  		if len(s.docs) > s.cfg.HomeArticles {
   399  			d.Data = s.docs[:s.cfg.HomeArticles]
   400  		}
   401  		t = s.template.home
   402  	case "/index":
   403  		d.Data = s.docs
   404  		t = s.template.index
   405  	case "/feed.atom", "/feeds/posts/default":
   406  		w.Header().Set("Content-type", "application/atom+xml; charset=utf-8")
   407  		w.Write(s.atomFeed)
   408  		return
   409  	case "/.json":
   410  		if p := r.FormValue("jsonp"); validJSONPFunc.MatchString(p) {
   411  			w.Header().Set("Content-type", "application/javascript; charset=utf-8")
   412  			fmt.Fprintf(w, "%v(%s)", p, s.jsonFeed)
   413  			return
   414  		}
   415  		w.Header().Set("Content-type", "application/json; charset=utf-8")
   416  		w.Write(s.jsonFeed)
   417  		return
   418  	default:
   419  		doc, ok := s.docPaths[p]
   420  		if !ok {
   421  			// Not a doc; try to just serve static content.
   422  			s.content.ServeHTTP(w, r)
   423  			return
   424  		}
   425  		d.Doc = doc
   426  		t = s.template.article
   427  	}
   428  	var err error
   429  	if s.cfg.ServeLocalLinks {
   430  		var buf bytes.Buffer
   431  		err = t.ExecuteTemplate(&buf, "root", d)
   432  		if err != nil {
   433  			log.Println(err)
   434  			return
   435  		}
   436  		_, err = golangOrgAbsLinkReplacer.WriteString(w, buf.String())
   437  	} else {
   438  		err = t.ExecuteTemplate(w, "root", d)
   439  	}
   440  	if err != nil {
   441  		log.Println(err)
   442  	}
   443  }
   444  
   445  // docsByTime implements sort.Interface, sorting Docs by their Time field.
   446  type docsByTime []*Doc
   447  
   448  func (s docsByTime) Len() int           { return len(s) }
   449  func (s docsByTime) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
   450  func (s docsByTime) Less(i, j int) bool { return s[i].Time.After(s[j].Time) }
   451  
   452  // notExist reports whether the path exists or not.
   453  func notExist(path string) bool {
   454  	_, err := os.Stat(path)
   455  	return os.IsNotExist(err)
   456  }