-
-
Notifications
You must be signed in to change notification settings - Fork 7.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Standardize author data #1850
Standardize author data #1850
Changes from all commits
d086a22
efed35e
71cc163
579074d
f1e5e3c
4bca853
c54196b
d118426
f859ce9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,23 +13,58 @@ | |
|
||
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 | ||
} | ||
|
||
// AuthorSocial is a place to put social details per author. These are the | ||
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 | ||
languages map[string]Author | ||
} | ||
|
||
// 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 +78,205 @@ type Author struct { | |
// - 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, 0, len(m)) | ||
for authorID, data := range m { | ||
authorMap, ok := data.(map[string]interface{}) | ||
if !ok { | ||
continue | ||
} | ||
a := mapToAuthor(authorID, authorMap) | ||
if a.ID != "" { | ||
authors = append(authors, a) | ||
} | ||
} | ||
sort.Stable(authors) | ||
return authors | ||
} | ||
|
||
func mapToAuthor(id string, m map[string]interface{}) Author { | ||
if id == "" { | ||
return Author{} | ||
} | ||
|
||
author := Author{ID: id} | ||
for k, data := range m { | ||
switch strings.ToLower(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 "weight": | ||
author.Weight = cast.ToInt(data) | ||
case "social": | ||
author.Social = normalizeSocial(cast.ToStringMapString(data)) | ||
case "params": | ||
author.Params = cast.ToStringMapString(data) | ||
case "languages": | ||
if author.languages == nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand your In my head the So, pseudo speaking:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Either there is a nested map like I have here inside the author, or you map authors by language and copy all the default info there. The way I have it implemented now doesn't copy any info, but incurs an extra if statement per access + 1 map access per nested property. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This construction is really verbose and error prone: if langAuthor.GivenName != "" {
a.GivenName = langAuthor.GivenName
} If I understand it correctly, an author can be defined in several places (page front matter, site config, /data) and some values can be overridden by more specific maps (language). Pseudo: author
for k, v := range lessSpecificMap
author[k] = v
for k, v := range moreSpecificMap
author[k] = v I cannot be more specific than that, I'm afraid. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Authors are only defined in If you don't have any specific suggestions, I will just leave the implementation as is, which works, is clear and is tested. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It isn't clear to me, but maybe @spf13 understands it. |
||
author.languages = make(map[string]Author) | ||
} | ||
langAuthorMap := cast.ToStringMap(data) | ||
for lang, m := range langAuthorMap { | ||
authorMap, ok := m.(map[string]interface{}) | ||
if !ok { | ||
continue | ||
} | ||
author.languages[lang] = mapToAuthor(id, authorMap) | ||
} | ||
} | ||
} | ||
|
||
// set a reasonable default for DisplayName | ||
if author.DisplayName == "" { | ||
author.DisplayName = author.GivenName + " " + author.FamilyName | ||
} | ||
|
||
return author | ||
} | ||
|
||
// languageOverride returns an author with details overridden by the provided language | ||
func (a Authors) languageOverride(lang string) Authors { | ||
langAuthors := make(Authors, 0, len(a)) | ||
for _, author := range a { | ||
langAuthors = append(langAuthors, author.languageOverride(lang)) | ||
} | ||
return langAuthors | ||
} | ||
|
||
// languageOverride returns an author with details overridden by the provided language | ||
func (a Author) languageOverride(lang string) Author { | ||
if a.languages == nil { | ||
return a | ||
} | ||
|
||
langAuthor, ok := a.languages[lang] | ||
if !ok { | ||
return a | ||
} | ||
|
||
if langAuthor.GivenName != "" { | ||
a.GivenName = langAuthor.GivenName | ||
} | ||
if langAuthor.FirstName != "" { | ||
a.FirstName = langAuthor.FirstName | ||
} | ||
if langAuthor.FamilyName != "" { | ||
a.FamilyName = langAuthor.FamilyName | ||
} | ||
if langAuthor.LastName != "" { | ||
a.LastName = langAuthor.LastName | ||
} | ||
if langAuthor.DisplayName != "" { | ||
a.DisplayName = langAuthor.DisplayName | ||
} | ||
if langAuthor.Thumbnail != "" { | ||
a.Thumbnail = langAuthor.Thumbnail | ||
} | ||
if langAuthor.Image != "" { | ||
a.Image = langAuthor.Image | ||
} | ||
if langAuthor.ShortBio != "" { | ||
a.ShortBio = langAuthor.ShortBio | ||
} | ||
if langAuthor.Bio != "" { | ||
a.Bio = langAuthor.Bio | ||
} | ||
if langAuthor.Email != "" { | ||
a.Email = langAuthor.Email | ||
} | ||
|
||
for k, v := range langAuthor.Social { | ||
a.Social[k] = v | ||
} | ||
|
||
for k, v := range langAuthor.Params { | ||
a.Params[k] = v | ||
} | ||
|
||
return a | ||
} | ||
|
||
// 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 { | ||
if !isSupportedSocialNetwork(network) { | ||
continue | ||
} | ||
|
||
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 isSupportedSocialNetwork(network string) bool { | ||
switch network { | ||
case | ||
"github", | ||
"facebook", | ||
"twitter", | ||
"googleplus", | ||
"pinterest", | ||
"instagram", | ||
"youtube", | ||
"linkedin": | ||
return true | ||
} | ||
return false | ||
} | ||
|
||
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 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't needed. The authors provided by node and page should already be sorted.