URI: 
       Add First Class Author Support - hugo - [fork] hugo port for 9front
  HTML git clone git@git.drkhsh.at/hugo.git
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
   DIR README
   DIR LICENSE
       ---
   DIR commit cf978c06496d99e76b08418422dda5797d90fed6
   DIR parent 44bf76d0f28cc88a1dc185d1c587e0977652edf2
  HTML Author: Derek Perkins <derek@derekperkins.com>
       Date:   Thu, 15 Sep 2016 20:28:13 -0600
       
       Add First Class Author Support
       
       Closes #1850
       
       Diffstat:
         M docs/content/templates/rss.md       |       5 +----
         M docs/content/templates/variables.md |       2 +-
         M hugolib/author.go                   |     157 ++++++++++++++++++++++++++++---
         M hugolib/node.go                     |      19 +++++++++++++++----
         M hugolib/page.go                     |      42 ++++++++++++++++++-------------
         M hugolib/site.go                     |       9 ++++++---
         M tpl/template_embedded.go            |       7 ++-----
       
       7 files changed, 195 insertions(+), 46 deletions(-)
       ---
   DIR diff --git 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>
   DIR diff --git 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>
   DIR diff --git 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 }
   DIR diff --git 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
       +}
   DIR diff --git 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 {
   DIR diff --git 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"),
   DIR diff --git 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>