You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

424 lines
10 KiB

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