From cf978c06496d99e76b08418422dda5797d90fed6 Mon Sep 17 00:00:00 2001 From: Derek Perkins Date: Thu, 15 Sep 2016 20:28:13 -0600 Subject: [PATCH] Add First Class Author Support Closes #1850 --- docs/content/templates/rss.md | 5 +- docs/content/templates/variables.md | 2 +- hugolib/author.go | 157 +++++++++++++++++++++++++--- hugolib/node.go | 19 +++- hugolib/page.go | 48 +++++---- hugolib/site.go | 9 +- tpl/template_embedded.go | 7 +- 7 files changed, 198 insertions(+), 49 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. {{ .Permalink }} Recent content {{ with .Title }}in {{.}} {{ end }}on {{ .Site.Title }} Hugo -- gohugo.io{{ with .Site.LanguageCode }} - {{.}}{{end}}{{ with .Site.Author.email }} - {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Author.email }} - {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Copyright }} + {{.}}{{end}}{{ with .Site.Copyright }} {{.}}{{end}}{{ if not .Date.IsZero }} {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}{{ end }} @@ -80,7 +78,6 @@ This is the default RSS template that ships with Hugo. It adheres to the [RSS 2. {{ .Title }} {{ .Permalink }} {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }} - {{ with .Site.Author.email }}{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}} {{ .Permalink }} {{ .Content | html }} 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.
**.Site.Menus** All of the menus in the site.
**.Site.Title** A string representing the title of the site.
-**.Site.Author** A map of the authors as defined in the site configuration.
+**.Site.Authors** An ordered list (ordered by defined weight) of the authors as defined in the site configuration.
**.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.
**.Site.DisqusShortname** A string representing the shortname of the Disqus shortcode as defined in the site configuration.
**.Site.GoogleAnalytics** A string representing your tracking code for Google Analytics as defined in the site configuration.
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 := authorKeys.([]string) - if len(authors) < 1 || len(p.Site.Authors) < 1 { - return AuthorList{} - } - - al := make(AuthorList) - for _, author := range authors { - a, ok := p.Site.Authors[author] - if ok { - al[author] = a +// 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} } } - return al + + authorIDs, ok := p.Params["authors"].([]string) + if !ok || len(authorIDs) == 0 || len(p.Site.Authors) == 0 { + return Authors{} + } + + 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 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", "") t.AddInternalShortcode("youtube.html", `{{ if .IsNamedParams }}
-
{{ else }}
@@ -70,9 +70,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() { {{ .Permalink }} Recent content {{ with .Title }}in {{.}} {{ end }}on {{ .Site.Title }} Hugo -- gohugo.io{{ with .Site.LanguageCode }} - {{.}}{{end}}{{ with .Site.Author.email }} - {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Author.email }} - {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Copyright }} + {{.}}{{end}}{{ with .Site.Copyright }} {{.}}{{end}}{{ if not .Date.IsZero }} {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}{{ end }} @@ -81,7 +79,6 @@ func (t *GoHTMLTemplate) EmbedTemplates() { {{ .Title }} {{ .Permalink }} {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }} - {{ with .Site.Author.email }}{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}} {{ .Permalink }} {{ .Content | html }}