Giant blob of minor changes
[dotfiles/.git] / .config / coc / extensions / coc-go-data / tools / pkg / mod / golang.org / x / tools@v0.0.0-20201105173854-bc9fc8d8c4bc / 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.
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         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.
56 type 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         Newer, Older *Doc
64 }
65
66 // Server implements an http.Handler that serves blog articles.
67 type 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                 home, index, article, doc *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.
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         // Parse templates.
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         // Load content.
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         // Set up content file server.
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 // 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.
151 func sectioned(d *present.Doc) bool {
152         return len(d.Sections) > 1
153 }
154
155 // authors returns a comma-separated list of author names.
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 // authorName returns the first line of the Author text: the author's name.
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 // 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.
192 func (s *Server) loadDocs(root string) error {
193         // Read content into docs field.
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)] // 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                         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         // 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.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         // Pull out unique sorted list of tags.
259         for t := range s.docTags {
260                 s.tags = append(s.tags, t)
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.Related, d)
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.
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                 // 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                         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 // renderJSONFeed generates a JSON feed and stores it in the Server's jsonFeed
371 // field.
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 // summary returns the first paragraph of text from the provided Doc.
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                         // 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.
418 type 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.
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                         // Not a doc; try to just serve static content.
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 // docsByTime implements sort.Interface, sorting Docs by their Time field.
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 // notExist reports whether the path exists or not.
499 func notExist(path string) bool {
500         _, err := os.Stat(path)
501         return os.IsNotExist(err)
502 }