From f40dd40358ac6b3f1b81ef8753a184307caa2129 Mon Sep 17 00:00:00 2001 From: Adrien Poupin Date: Fri, 28 Apr 2017 11:06:05 +0200 Subject: [PATCH] Add a widget mechanism to Hugo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 -}}
Here is a text widget, but there is nothing to print. Please define options.content inside every text widget in your config.
{{- end -}} ``` 3. Configure your site with a `widgets` variable describing widgets inside widget areas: ``` widgets: sidebar: - type: text options: content: "

IT WORKS from config

" 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 #2683 See #2535 --- helpers/path.go | 6 ++ helpers/pathspec.go | 7 ++ hugolib/config.go | 1 + hugolib/hugo_sites.go | 12 +++ hugolib/site.go | 15 ++++ hugolib/widget.go | 145 +++++++++++++++++++++++++++++++ tpl/partials/partials.go | 10 ++- tpl/tplimpl/templateFuncster.go | 36 +++++++- tpl/tplimpl/template_embedded.go | 19 ++++ tpl/tplimpl/template_funcs.go | 1 + tpl/widget/init.go | 49 +++++++++++ tpl/widget/widget.go | 59 +++++++++++++ 12 files changed, 358 insertions(+), 2 deletions(-) create mode 100644 hugolib/widget.go create mode 100644 tpl/widget/init.go create mode 100644 tpl/widget/widget.go diff --git a/helpers/path.go b/helpers/path.go index 57f02da685b..6a066dd1623 100644 --- a/helpers/path.go +++ b/helpers/path.go @@ -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 { diff --git a/helpers/pathspec.go b/helpers/pathspec.go index 164d242a0d3..bebe77a7b86 100644 --- a/helpers/pathspec.go +++ b/helpers/pathspec.go @@ -40,6 +40,7 @@ type PathSpec struct { // Directories themesDir string layoutDir string + widgetsDir string workingDir string staticDirs []string @@ -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"), @@ -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 diff --git a/hugolib/config.go b/hugolib/config.go index da84ab8b25c..8e2b70e936d 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -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") diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index bf488b9be75..a12f827fae0 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -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 } } diff --git a/hugolib/site.go b/hugolib/site.go index 936584580f4..b03542dc923 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -97,6 +97,7 @@ type Site struct { Sections Taxonomy Info SiteInfo Menus Menus + Widgets Widgets timer *nitro.B layoutHandler *output.LayoutHandler @@ -357,6 +358,7 @@ type SiteInfo struct { *PageCollections Files *[]*source.File Menus *Menus + Widgets *Widgets Hugo *HugoInfo Title string RSSLink string @@ -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) @@ -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") } diff --git a/hugolib/widget.go b/hugolib/widget.go new file mode 100644 index 00000000000..c3c61c7cbd4 --- /dev/null +++ b/hugolib/widget.go @@ -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 +} diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go index beb09f426bc..c6d4bcee535 100644 --- a/tpl/partials/partials.go +++ b/tpl/partials/partials.go @@ -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" @@ -51,6 +52,7 @@ 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:] } @@ -58,11 +60,17 @@ func (ns *Namespace) Include(name string, contextList ...interface{}) (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. diff --git a/tpl/tplimpl/templateFuncster.go b/tpl/tplimpl/templateFuncster.go index e6bbde8ecd2..05ca50aacfb 100644 --- a/tpl/tplimpl/templateFuncster.go +++ b/tpl/tplimpl/templateFuncster.go @@ -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. @@ -39,6 +41,7 @@ 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:] } @@ -46,11 +49,17 @@ func (t *templateFuncster) partial(name string, contextList ...interface{}) (int 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. @@ -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) +} +*/ diff --git a/tpl/tplimpl/template_embedded.go b/tpl/tplimpl/template_embedded.go index 2252d65cf73..61214a0f501 100644 --- a/tpl/tplimpl/template_embedded.go +++ b/tpl/tplimpl/template_embedded.go @@ -56,6 +56,7 @@ func (t *templateHandler) embedShortcodes() { t.addInternalShortcode("gist.html", ``) 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() { @@ -165,6 +166,24 @@ func (t *templateHandler) embedTemplates() { {{ end }}`) + t.addInternalTemplate("", "widgets.html", `{{- range .c.Site.Widgets -}} +{{- if eq .Name $._wa -}}{{/* Display only the current widget area */}} +
+ {{- $waname := .Name -}} + {{ range .Widgets -}} +
+ {{ $context := (dict "$" $.c "wa" $._wa "w" .Type "Params" .Params) }} + {{ partial (print .Type "/widget.html") "widgets" $context }} +
+ {{- end }}{{/* end range widgets */}} +
+{{/* end if */}}{{- end -}} +{{/* end range widget areas */}}{{- end -}}`) + + t.addInternalTemplate("widgets", "text/widget.html", `{{- if isset .Params "content" -}} + {{- .Params.content | safeHTML -}} +{{- else -}}
Here is a text widget, but there is nothing to print. Please define options.content inside every text widget in your config.
+{{- end -}}`) t.addInternalTemplate("", "disqus.html", `{{ if .Site.DisqusShortname }}