Skip to content

Commit

Permalink
Add a widget mechanism to Hugo
Browse files Browse the repository at this point in the history
Here is a first attempt to get widgets into Hugo. General tests are ok on my dev environment, but I have not already written tests for widgets.

1. Create a site
2. Create a widget directory inside `/widgets`. It should look like this:
```
widgets/
└── text
    ├── layouts
    │   └── widget.html
    └── README.md
```
Note that the name `widget.html` is mandatory. *Currently the context is the content of the config parameter `widgets.[mywidgetarea].[mywidget].options`*. Variables are accessible with `.content` for a text widget like the following and as described in the config below.
```
{{- if isset . "content" -}}
  {{- .content | safeHTML -}}
{{- else -}}<pre>Here is a text widget, but there is nothing to print. Please define options.content inside every text widget in your config.</pre>
{{- end -}}
```

3. Configure your site with a `widgets` variable describing widgets inside widget areas:
```
widgets:
  sidebar:
    - type: text
      options:
        content: "<h1>IT WORKS from config</h1>"
        parser: html
  showcase:
    - type: text
      options:
        content: "Here lies a showcase."
  footer:
    - type: text
      options:
        content: "Powered by Hugo with widgets."
        foo: bar
```

4. Create a template using the `widgets` call. This can be done like this: `{{ widgets "sidebar" . }}`.
5. Create content. You can also use the widget's shortcode: `{{% widgets "showcase" %}}`
6. Build and enjoy.

- Currently the widgets' context is only the content of the config variable. We should add a wider context (easy).
- I have not studied the impact on performances.
- Else?

Fixes gohugoio#2683
See gohugoio#2535
  • Loading branch information
lebarde committed Nov 26, 2017
1 parent 288723a commit f40dd40
Show file tree
Hide file tree
Showing 12 changed files with 358 additions and 2 deletions.
6 changes: 6 additions & 0 deletions helpers/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@ func (p *PathSpec) GetLayoutDirPath() string {
return p.AbsPathify(p.layoutDir)
}

// GetWidgetsDirPath returns the absolute path to the widgets'
// directory for the current Hugo project.
func (p *PathSpec) GetWidgetsDirPath() string {
return p.AbsPathify(p.widgetsDir)
}

// GetThemeDir gets the root directory of the current theme, if there is one.
// If there is no theme, returns the empty string.
func (p *PathSpec) GetThemeDir() string {
Expand Down
7 changes: 7 additions & 0 deletions helpers/pathspec.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type PathSpec struct {
// Directories
themesDir string
layoutDir string
widgetsDir string
workingDir string
staticDirs []string

Expand Down Expand Up @@ -93,6 +94,7 @@ func NewPathSpec(fs *hugofs.Fs, cfg config.Provider) (*PathSpec, error) {
BaseURL: baseURL,
themesDir: cfg.GetString("themesDir"),
layoutDir: cfg.GetString("layoutDir"),
widgetsDir: cfg.GetString("widgetsDir"),
workingDir: cfg.GetString("workingDir"),
staticDirs: staticDirs,
theme: cfg.GetString("theme"),
Expand Down Expand Up @@ -144,6 +146,11 @@ func (p *PathSpec) LayoutDir() string {
return p.layoutDir
}

// WidgetsDir returns the relative layout dir in the currenct Hugo project.
func (p *PathSpec) WidgetsDir() string {
return p.widgetsDir
}

// Theme returns the theme name if set.
func (p *PathSpec) Theme() string {
return p.theme
Expand Down
1 change: 1 addition & 0 deletions hugolib/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ func loadDefaultSettingsFor(v *viper.Viper) error {
v.SetDefault("contentDir", "content")
v.SetDefault("layoutDir", "layouts")
v.SetDefault("staticDir", "static")
v.SetDefault("widgetsDir", "widgets")
v.SetDefault("archetypeDir", "archetypes")
v.SetDefault("publishDir", "public")
v.SetDefault("dataDir", "data")
Expand Down
12 changes: 12 additions & 0 deletions hugolib/hugo_sites.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,18 @@ func (s *Site) withSiteTemplates(withTemplates ...func(templ tpl.TemplateHandler
}
}

// Here we handle the widgets. The site gets all HTML
// code to inject it inside the template, when the
// {{ widgets "mywidgetarea" }} is called.

// Load all templates placed in the widgets/ directory.
// TODO already done inside injectWidgets().
//templ.LoadTemplates(s.PathSpec.GetWidgetsDirPath(), "widgets")

if err := s.injectWidgets(templ); err != nil {
s.Log.ERROR.Printf("Failed to load widgets: %s", err)
}

return nil
}
}
Expand Down
15 changes: 15 additions & 0 deletions hugolib/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ type Site struct {
Sections Taxonomy
Info SiteInfo
Menus Menus
Widgets Widgets
timer *nitro.B

layoutHandler *output.LayoutHandler
Expand Down Expand Up @@ -357,6 +358,7 @@ type SiteInfo struct {
*PageCollections
Files *[]*source.File
Menus *Menus
Widgets *Widgets
Hugo *HugoInfo
Title string
RSSLink string
Expand Down Expand Up @@ -1060,6 +1062,7 @@ func (s *Site) initializeSiteInfo() {
Data: &s.Data,
owner: s.owner,
s: s,
Widgets: &s.Widgets,
}

rssOutputFormat, found := s.outputFormats[KindHome].GetByName(output.RSSFormat.Name)
Expand Down Expand Up @@ -1121,6 +1124,18 @@ func (s *Site) getThemeDataDir(path string) string {
return s.getRealDir(filepath.Join(s.PathSpec.GetThemeDir(), s.dataDir()), path)
}

func (s *Site) widgetDir() string {
return s.Cfg.GetString("widgetsDir")
}

func (s *Site) getWidgetDir(path string) string {
return s.getRealDir(s.Cfg.GetString("widgetDir"), path)
}

func (s *Site) isWidgetDirEvent(e fsnotify.Event) bool {
return s.getWidgetDir(e.Name) != ""
}

func (s *Site) layoutDir() string {
return s.Cfg.GetString("layoutDir")
}
Expand Down
145 changes: 145 additions & 0 deletions hugolib/widget.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright 2016-present The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package hugolib

import (
"html/template"

"github.com/spf13/cast"
"github.com/gohugoio/hugo/tpl"
)

// TODO See the shortcode system to see the structure

// Data structures and methods
// ===========================

// A WidgetEntry represents a widget item defined
// in the site config.
// (TODO: See hugolib/menu.go for the data structure)
type Widget struct {
Type string
Params map[string]interface{}
Identifier string
Weight int
Template *template.Template
}

func newWidget(widgetType string, options interface{}) (*Widget, error) {
return &Widget{Type: widgetType, Params: options.(map[string]interface{})}, nil
}

type WidgetArea struct {
Name string
Widgets []*Widget
Template *template.Template
}

func newWidgetArea(waname string) *WidgetArea {
// TODO ??
return &WidgetArea{Name: waname, Widgets: nil, Template: nil}
}

type Widgets map[string]*WidgetArea

// Internal widgets building
// =========================

// WidgetsConfig parses the widgets config variable
// (is a collection) and calls every widget configuration.
func (s *Site) getWidgetsFromConfig() Widgets {
ret := Widgets{}

if conf := s.Cfg.GetStringMap("widgets"); conf != nil {
for waname, widgetarea := range conf {
// wa is a widget area defined in the conf file
wa, err := cast.ToSliceE(widgetarea)
if err != nil {
s.Log.ERROR.Printf("unable to process widgets in site config\n")
s.Log.ERROR.Println(err)
}

// Instantiate a WidgetArea
waobj := newWidgetArea(waname)

// Retrieve all widgets
for _, w := range wa {
iw, err := cast.ToStringMapE(w)

if err != nil {
s.Log.ERROR.Printf("unable to process widget inside widget area in site config\n")
s.Log.ERROR.Println(err)
}

// iw represents a widget inside a widget area
wtype := cast.ToString(iw["type"])
woptions, err := cast.ToStringMapE(iw["options"])
wobj, err := newWidget(wtype, woptions)

if err != nil {
s.Log.ERROR.Printf("unable to instantiate widget: %s\n", iw)
s.Log.ERROR.Println(err)
}

// then append it to the widget area object
waobj.Widgets = append(waobj.Widgets, wobj)
}

// don't forget to append that widget area to the
// Widgets object
ret[waname] = waobj
}
}

return ret
}

// instantiateWidget retrieves the widget's files
// and creates the templates
func (s *Site) instantiateWidget(temp tpl.TemplateHandler, wa *WidgetArea, w *Widget) *Widget {
// Load this widget's templates
// using the site object's owner.tmpl
temp.LoadTemplates(s.PathSpec.GetWidgetsDirPath()+"/"+w.Type+"/layouts", "widgets/"+w.Type)

return w
}

// Main widgets entry point
// ========================

// This function adds the whole widgets' template code
// in the Site object. This is of type template.HTML.
// This function is called from hugo_sites.
func (s *Site) injectWidgets(temp tpl.TemplateHandler) error {
// Get widgets. This gives all information we need but
// does not already read widget files.
widgets := s.getWidgetsFromConfig()

for _, widgetarea := range widgets {
// _ is waname, if ever we need

for _, w := range widgetarea.Widgets {
w = s.instantiateWidget(temp, widgetarea, w)
}
}

// We now have all widgets with their templates.
// Generate all widget areas with their templates
// Now the template's content will be used inside
// the main template files inside tpl/template_funcs
// and in the templates using {{ widgets "mywidgetarea" }}
s.Widgets = widgets

return nil
}
10 changes: 9 additions & 1 deletion tpl/partials/partials.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"strings"
"sync"
texttemplate "text/template"
"github.com/spf13/cast"

bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/deps"
Expand Down Expand Up @@ -51,18 +52,25 @@ type Namespace struct {
// Include executes the named partial and returns either a string,
// when the partial is a text/template, or template.HTML when html/template.
func (ns *Namespace) Include(name string, contextList ...interface{}) (interface{}, error) {
var prefix = "partials"
if strings.HasPrefix("partials/", name) {
name = name[8:]
}
var context interface{}

if len(contextList) == 0 {
context = nil
} else if pr, err := cast.ToStringE(contextList[0]); err == nil && len(contextList) >= 2 {
// The first parameter of the list (second of the partial
// call) is the prefix
prefix = pr
context = contextList[1]
} else {
context = contextList[0]
}

for _, n := range []string{"partials/" + name, "theme/partials/" + name} {
prefix += "/"
for _, n := range []string{prefix + name, "theme/" + prefix + name} {
templ := ns.deps.Tmpl.Lookup(n)
if templ == nil {
// For legacy reasons.
Expand Down
36 changes: 35 additions & 1 deletion tpl/tplimpl/templateFuncster.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (

bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/deps"

"github.com/spf13/cast"
)

// Some of the template funcs are'nt entirely stateless.
Expand All @@ -39,18 +41,25 @@ func newTemplateFuncster(deps *deps.Deps) *templateFuncster {
// Partial executes the named partial and returns either a string,
// when called from text/template, for or a template.HTML.
func (t *templateFuncster) partial(name string, contextList ...interface{}) (interface{}, error) {
var prefix = "partials"
if strings.HasPrefix("partials/", name) {
name = name[8:]
}
var context interface{}

if len(contextList) == 0 {
context = nil
} else if pr, err := cast.ToStringE(contextList[0]); err == nil && len(contextList) >= 2 {
// The first parameter of the list (second of the partial
// call) is the prefix
prefix = pr
context = contextList[1]
} else {
context = contextList[0]
}

for _, n := range []string{"partials/" + name, "theme/partials/" + name} {
prefix += "/"
for _, n := range []string{prefix + name, "theme/" + prefix + name} {
templ := t.Tmpl.Lookup(n)
if templ == nil {
// For legacy reasons.
Expand All @@ -75,3 +84,28 @@ func (t *templateFuncster) partial(name string, contextList ...interface{}) (int

return "", fmt.Errorf("Partial %q not found", name)
}

/*
// Retrieves and display a widget area using the /widgets/ shortcode
func (t *templateFuncster) widgets(name string, context interface{}) (interface{}, error) {
// Add (_wa: name) index/value to context to access it inside
// the embedded template (as Widget Area)
outcontext := make(map[string]interface{})
outcontext["c"] = context
outcontext["_wa"] = name
// See in template_embedded for widgets.html
templ := t.Tmpl.Lookup("_internal/widgets.html")
if templ != nil {
b := bp.GetBuffer()
defer bp.PutBuffer(b)
if err := templ.Execute(b, outcontext); err != nil {
return "", err
}
return template.HTML(b.String()), nil
}
return "", fmt.Errorf("Widget area %q not found", name)
}
*/
19 changes: 19 additions & 0 deletions tpl/tplimpl/template_embedded.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func (t *templateHandler) embedShortcodes() {
t.addInternalShortcode("gist.html", `<script src="//gist.github.com/{{ index .Params 0 }}/{{ index .Params 1 }}.js{{if len .Params | eq 3 }}?file={{ index .Params 2 }}{{end}}"></script>`)
t.addInternalShortcode("tweet.html", `{{ (getJSON "https://api.twitter.com/1/statuses/oembed.json?id=" (index .Params 0)).html | safeHTML }}`)
t.addInternalShortcode("instagram.html", `{{ if len .Params | eq 2 }}{{ if eq (.Get 1) "hidecaption" }}{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=1" }}{{ .html | safeHTML }}{{ end }}{{ end }}{{ else }}{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=0" }}{{ .html | safeHTML }}{{ end }}{{ end }}`)
t.addInternalShortcode("widgets.html", `{{ widgets (.Get 0) $ }}`)
}

func (t *templateHandler) embedTemplates() {
Expand Down Expand Up @@ -165,6 +166,24 @@ func (t *templateHandler) embedTemplates() {
</ul>
{{ end }}`)

t.addInternalTemplate("", "widgets.html", `{{- range .c.Site.Widgets -}}
{{- if eq .Name $._wa -}}{{/* Display only the current widget area */}}
<div class="widget-area widget-area-{{ .Name }}">
{{- $waname := .Name -}}
{{ range .Widgets -}}
<div class="widget widget-{{ .Type }}">
{{ $context := (dict "$" $.c "wa" $._wa "w" .Type "Params" .Params) }}
{{ partial (print .Type "/widget.html") "widgets" $context }}
</div>
{{- end }}{{/* end range widgets */}}
</div>
{{/* end if */}}{{- end -}}
{{/* end range widget areas */}}{{- end -}}`)

t.addInternalTemplate("widgets", "text/widget.html", `{{- if isset .Params "content" -}}
{{- .Params.content | safeHTML -}}
{{- else -}}<pre>Here is a text widget, but there is nothing to print. Please define options.content inside every text widget in your config.</pre>
{{- end -}}`)
t.addInternalTemplate("", "disqus.html", `{{ if .Site.DisqusShortname }}<div id="disqus_thread"></div>
<script>
var disqus_config = function () {
Expand Down
1 change: 1 addition & 0 deletions tpl/tplimpl/template_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
_ "github.com/gohugoio/hugo/tpl/time"
_ "github.com/gohugoio/hugo/tpl/transform"
_ "github.com/gohugoio/hugo/tpl/urls"
_ "github.com/gohugoio/hugo/tpl/widget"
)

func (t *templateFuncster) initFuncMap() {
Expand Down
Loading

0 comments on commit f40dd40

Please sign in to comment.