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