Skip to content
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

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 251 additions & 14 deletions hugolib/author.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Member

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.

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
Expand All @@ -43,3 +78,205 @@ 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, 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand your languages implementation -- but looking at the code below, with the languageOverride func, it suspect it is too elaborate. We have widespread way of configuration overrides of maps in Hugo (and Viper), and this should be done at configuration load time.

In my head the languages in the author context isn't languages the author speaks, but different configurations for the different languages (aka different shortbio in Norwegian etc.)

So, pseudo speaking:

# defaults 
givenName = "bep"
bio = "Developer from Norway"

[languages]
[languages.no]
bio = "Utvikler fra Norge"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

languages functions exactly how you describe. All it does is take the language set on the Node, then does a lookup on properties and replaces them in the defaults if the language specific property isn't empty. If you would like it implemented in a different fashion, show me a configuration override that you'd rather have me use.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authors are only defined in /data/_authors and the author key/ID is referenced in front matter.

If you don't have any specific suggestions, I will just leave the implementation as is, which works, is clear and is tested.

Copy link
Member

Choose a reason for hiding this comment

The 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 }
Loading