diff options
author | Derek Perkins <derek@derekperkins.com> | 2016-09-15 20:28:13 -0600 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2016-09-17 00:49:24 +0200 |
commit | cf978c06496d99e76b08418422dda5797d90fed6 (patch) | |
tree | 301a0eb848539cef0ae7226d0816f37767d69acc | |
parent | 44bf76d0f28cc88a1dc185d1c587e0977652edf2 (diff) |
Add First Class Author Support
Closes #1850
-rw-r--r-- | docs/content/templates/rss.md | 5 | ||||
-rw-r--r-- | docs/content/templates/variables.md | 2 | ||||
-rw-r--r-- | hugolib/author.go | 157 | ||||
-rw-r--r-- | hugolib/node.go | 19 | ||||
-rw-r--r-- | hugolib/page.go | 42 | ||||
-rw-r--r-- | hugolib/site.go | 9 | ||||
-rw-r--r-- | tpl/template_embedded.go | 7 |
7 files changed, 195 insertions, 46 deletions
diff --git a/docs/content/templates/rss.md b/docs/content/templates/rss.md index 70c3c7704..60e09ccf1 100644 --- a/docs/content/templates/rss.md +++ b/docs/content/templates/rss.md @@ -69,9 +69,7 @@ This is the default RSS template that ships with Hugo. It adheres to the [RSS 2. <link>{{ .Permalink }}</link> <description>Recent content {{ with .Title }}in {{.}} {{ end }}on {{ .Site.Title }}</description> <generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }} - <language>{{.}}</language>{{end}}{{ with .Site.Author.email }} - <managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }} - <webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }} + <language>{{.}}</language>{{end}}{{ with .Site.Copyright }} <copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }} <lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }} <atom:link href="{{.URL}}" rel="self" type="application/rss+xml" /> @@ -80,7 +78,6 @@ This is the default RSS template that ships with Hugo. It adheres to the [RSS 2. <title>{{ .Title }}</title> <link>{{ .Permalink }}</link> <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate> - {{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}} <guid>{{ .Permalink }}</guid> <description>{{ .Content | html }}</description> </item> diff --git a/docs/content/templates/variables.md b/docs/content/templates/variables.md index b85387b46..12f940f17 100644 --- a/docs/content/templates/variables.md +++ b/docs/content/templates/variables.md @@ -168,7 +168,7 @@ Also available is `.Site` which has the following: **.Site.Files** All of the source files of the site.<br> **.Site.Menus** All of the menus in the site.<br> **.Site.Title** A string representing the title of the site.<br> -**.Site.Author** A map of the authors as defined in the site configuration.<br> +**.Site.Authors** An ordered list (ordered by defined weight) of the authors as defined in the site configuration.<br> **.Site.LanguageCode** A string representing the language as defined in the site configuration. This is mostly used to populate the RSS feeds with the right language code.<br> **.Site.DisqusShortname** A string representing the shortname of the Disqus shortcode as defined in the site configuration.<br> **.Site.GoogleAnalytics** A string representing your tracking code for Google Analytics as defined in the site configuration.<br> diff --git a/hugolib/author.go b/hugolib/author.go index 0f4327097..2fbbfe793 100644 --- a/hugolib/author.go +++ b/hugolib/author.go @@ -13,23 +13,57 @@ package hugolib -// AuthorList is a list of all authors and their metadata. -type AuthorList map[string]Author +import ( + "fmt" + "regexp" + "sort" + "strings" + + "github.com/spf13/cast" +) + +var ( + onlyNumbersRegExp = regexp.MustCompile("^[0-9]*$") +) + +// Authors is a list of all authors and their metadata. +type Authors []Author + +// Get returns an author from an ID +func (a Authors) Get(id string) Author { + for _, author := range a { + if author.ID == id { + return author + } + } + return Author{} +} + +// Sort sorts the authors by weight +func (a Authors) Sort() Authors { + sort.Stable(a) + return a +} // Author contains details about the author of a page. type Author struct { - GivenName string - FamilyName string - DisplayName string - Thumbnail string - Image string - ShortBio string - LongBio string - Email string - Social AuthorSocial + ID string + GivenName string // givenName OR firstName + FirstName string // alias for GivenName + FamilyName string // familyName OR lastName + LastName string // alias for FamilyName + DisplayName string // displayName + Thumbnail string // thumbnail + Image string // image + ShortBio string // shortBio + Bio string // bio + Email string // email + Social AuthorSocial // social + Params map[string]string // params + Weight int } -// AuthorSocial is a place to put social details per author. These are the +// AuthorSocial is a place to put social usernames per author. These are the // standard keys that themes will expect to have available, but can be // expanded to any others on a per site basis // - website @@ -43,3 +77,102 @@ type Author struct { // - linkedin // - skype type AuthorSocial map[string]string + +// URL is a convenience function that provides the correct canonical URL +// for a specific social network given a username. If an unsupported network +// is requested, only the username is returned +func (as AuthorSocial) URL(key string) string { + switch key { + case "github": + return fmt.Sprintf("https://github.com/%s", as[key]) + case "facebook": + return fmt.Sprintf("https://www.facebook.com/%s", as[key]) + case "twitter": + return fmt.Sprintf("https://twitter.com/%s", as[key]) + case "googleplus": + isNumeric := onlyNumbersRegExp.Match([]byte(as[key])) + if isNumeric { + return fmt.Sprintf("https://plus.google.com/%s", as[key]) + } + return fmt.Sprintf("https://plus.google.com/+%s", as[key]) + case "pinterest": + return fmt.Sprintf("https://www.pinterest.com/%s/", as[key]) + case "instagram": + return fmt.Sprintf("https://www.instagram.com/%s/", as[key]) + case "youtube": + return fmt.Sprintf("https://www.youtube.com/user/%s", as[key]) + case "linkedin": + return fmt.Sprintf("https://www.linkedin.com/in/%s", as[key]) + default: + return as[key] + } +} + +func mapToAuthors(m map[string]interface{}) Authors { + authors := make(Authors, len(m)) + for authorID, data := range m { + authorMap, ok := data.(map[string]interface{}) + if !ok { + continue + } + authors = append(authors, mapToAuthor(authorID, authorMap)) + } + sort.Stable(authors) + return authors +} + +func mapToAuthor(id string, m map[string]interface{}) Author { + author := Author{ID: id} + for k, data := range m { + switch k { + case "givenName", "firstName": + author.GivenName = cast.ToString(data) + author.FirstName = author.GivenName + case "familyName", "lastName": + author.FamilyName = cast.ToString(data) + author.LastName = author.FamilyName + case "displayName": + author.DisplayName = cast.ToString(data) + case "thumbnail": + author.Thumbnail = cast.ToString(data) + case "image": + author.Image = cast.ToString(data) + case "shortBio": + author.ShortBio = cast.ToString(data) + case "bio": + author.Bio = cast.ToString(data) + case "email": + author.Email = cast.ToString(data) + case "social": + author.Social = normalizeSocial(cast.ToStringMapString(data)) + case "params": + author.Params = cast.ToStringMapString(data) + } + } + + // set a reasonable default for DisplayName + if author.DisplayName == "" { + author.DisplayName = author.GivenName + " " + author.FamilyName + } + + return author +} + +// normalizeSocial makes a naive attempt to normalize social media usernames +// and strips out extraneous characters or url info +func normalizeSocial(m map[string]string) map[string]string { + for network, username := range m { + username = strings.TrimSpace(username) + username = strings.TrimSuffix(username, "/") + strs := strings.Split(username, "/") + username = strs[len(strs)-1] + username = strings.TrimPrefix(username, "@") + username = strings.TrimPrefix(username, "+") + m[network] = username + } + return m +} + +func (a Authors) Len() int { return len(a) } +func (a Authors) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a Authors) Less(i, j int) bool { return a[i].Weight < a[j].Weight } diff --git a/hugolib/node.go b/hugolib/node.go index 566fd4799..820c483a6 100644 --- a/hugolib/node.go +++ b/hugolib/node.go @@ -21,11 +21,9 @@ import ( "sync" "time" - jww "github.com/spf13/jwalterweatherman" - - "github.com/spf13/hugo/helpers" - "github.com/spf13/cast" + "github.com/spf13/hugo/helpers" + jww "github.com/spf13/jwalterweatherman" ) type Node struct { @@ -322,3 +320,16 @@ func (n *Node) addLangFilepathPrefix(outfile string) string { } return helpers.FilePathSeparator + filepath.Join(n.Lang(), outfile) } + +// Author returns the first defined author, sorted by Weight +func (n *Node) Author() Author { + if len(n.Site.Authors) == 0 { + return Author{} + } + return n.Site.Authors[0] +} + +// Authors returns all defined authors, sorted by Weight +func (n *Node) Authors() Authors { + return n.Site.Authors +} diff --git a/hugolib/page.go b/hugolib/page.go index fe4cd077f..6c6e984b4 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -190,33 +190,41 @@ func (p *Page) Param(key interface{}) (interface{}, error) { return p.Site.Params[keyStr], nil } +// Author returns the first listed author for a page func (p *Page) Author() Author { authors := p.Authors() - - for _, author := range authors { - return author + if len(authors) == 0 { + return Author{} } - return Author{} + return authors[0] } -func (p *Page) Authors() AuthorList { - authorKeys, ok := p.Params["authors"] - if !ok { - return AuthorList{} +// Authors returns all listed authors for a page in the order they +// are defined in the front matter. It first checks for a single author +// since that it the most common use case, then checks for multiple authors. +func (p *Page) Authors() Authors { + authorID, ok := p.Params["author"].(string) + if ok { + a := p.Site.Authors.Get(authorID) + if a.ID == authorID { + return Authors{a} + } } - authors := authorKeys.([]string) - if len(authors) < 1 || len(p.Site.Authors) < 1 { - return AuthorList{} + + authorIDs, ok := p.Params["authors"].([]string) + if !ok || len(authorIDs) == 0 || len(p.Site.Authors) == 0 { + return Authors{} } - al := make(AuthorList) - for _, author := range authors { - a, ok := p.Site.Authors[author] - if ok { - al[author] = a + authors := make([]Author, 0, len(authorIDs)) + for _, authorID := range authorIDs { + a := p.Site.Authors.Get(authorID) + if a.ID == authorID { + authors = append(authors, a) } } - return al + + return authors } func (p *Page) UniqueID() string { diff --git a/hugolib/site.go b/hugolib/site.go index 87c440d38..f7872ba99 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -165,7 +165,7 @@ type SiteInfo struct { BaseURL template.URL Taxonomies TaxonomyList - Authors AuthorList + Authors Authors Social SiteSocial Sections Taxonomy Pages *Pages // Includes only pages in this language @@ -176,7 +176,6 @@ type SiteInfo struct { Hugo *HugoInfo Title string RSSLink string - Author map[string]interface{} LanguageCode string DisqusShortname string GoogleAnalytics string @@ -733,6 +732,11 @@ func (s *Site) readDataFromSourceFS() error { } err = s.loadData(dataSources) + + // extract author data from /data/_authors then delete it from .Data + s.Info.Authors = mapToAuthors(cast.ToStringMap(s.Data["_authors"])) + delete(s.Data, "_authors") + s.timerStep("load data") return err } @@ -908,7 +912,6 @@ func (s *Site) initializeSiteInfo() { s.Info = SiteInfo{ BaseURL: template.URL(helpers.SanitizeURLKeepTrailingSlash(viper.GetString("BaseURL"))), Title: lang.GetString("Title"), - Author: lang.GetStringMap("author"), Social: lang.GetStringMapString("social"), LanguageCode: lang.GetString("languagecode"), Copyright: lang.GetString("copyright"), diff --git a/tpl/template_embedded.go b/tpl/template_embedded.go index c418511ac..185f7aecd 100644 --- a/tpl/template_embedded.go +++ b/tpl/template_embedded.go @@ -44,7 +44,7 @@ func (t *GoHTMLTemplate) EmbedShortcodes() { t.AddInternalShortcode("speakerdeck.html", "<script async class='speakerdeck-embed' data-id='{{ index .Params 0 }}' data-ratio='1.33333333333333' src='//speakerdeck.com/assets/embed.js'></script>") t.AddInternalShortcode("youtube.html", `{{ if .IsNamedParams }} <div {{ if .Get "class" }}class="{{ .Get "class" }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}> - <iframe src="//www.youtube.com/embed/{{ .Get "id" }}?{{ with .Get "autoplay" }}{{ if eq . "true" }}autoplay=1{{ end }}{{ end }}" + <iframe src="//www.youtube.com/embed/{{ .Get "id" }}?{{ with .Get "autoplay" }}{{ if eq . "true" }}autoplay=1{{ end }}{{ end }}" {{ if not (.Get "class") }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" {{ end }}allowfullscreen frameborder="0"></iframe> </div>{{ else }} <div {{ if len .Params | eq 2 }}class="{{ .Get 1 }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}> @@ -70,9 +70,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() { <link>{{ .Permalink }}</link> <description>Recent content {{ with .Title }}in {{.}} {{ end }}on {{ .Site.Title }}</description> <generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }} - <language>{{.}}</language>{{end}}{{ with .Site.Author.email }} - <managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }} - <webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }} + <language>{{.}}</language>{{end}}{{ with .Site.Copyright }} <copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }} <lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }} <atom:link href="{{.Permalink}}" rel="self" type="application/rss+xml" /> @@ -81,7 +79,6 @@ func (t *GoHTMLTemplate) EmbedTemplates() { <title>{{ .Title }}</title> <link>{{ .Permalink }}</link> <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate> - {{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}} <guid>{{ .Permalink }}</guid> <description>{{ .Content | html }}</description> </item> |