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) }