New plugin types: UISummary and UIFooter

This commit is contained in:
Jan-Lukas Else 2023-03-10 15:14:50 +01:00
parent 144e7f4a41
commit 8c9d17006d
11 changed files with 200 additions and 61 deletions

View File

@ -27,13 +27,15 @@ You need to specify the path to the plugin (remember to mount the path to your G
## Types of plugins
- `SetApp` (Access more GoBlog functionalities like the database) - see https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#SetApp
- `SetConfig` (Access the configuration provided for the plugin) - see https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#SetConfig
- `SetApp` (Access more GoBlog functionalities like the database) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#SetApp]
- `SetConfig` (Access the configuration provided for the plugin) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#SetConfig]
- `Exec` (Command that is executed in a Go routine when starting GoBlog) - see https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#Exec
- `Middleware` (HTTP middleware to intercept or modify HTTP requests) - see https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#Middleware
- `UI` (Modify rendered HTML) - see https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#UI
- `UI2` (Modify rendered HTML using a goquery document which improves performance and avoids multiple HTML parsing and rendering when using multiple plugins) - see https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#UI2
- `Exec` (Command that is executed in a Go routine when starting GoBlog) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#Exec]
- `Middleware` (HTTP middleware to intercept or modify HTTP requests) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/]plugintypes#Middleware
- `UI` (Modify rendered HTML) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#UI]
- `UI2` (Modify rendered HTML using a goquery document which improves performance and avoids multiple HTML parsing and rendering when using multiple plugins) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#UI2]
- `UISummary` (like UI2 for only the post summary on indexes) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#UISummary]
- `UIFooter` (like UI2 for only the post summary on indexes) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#UIFooter]
More types will be added later. Any plugin can implement multiple types, see the demo plugin as example.

View File

@ -39,18 +39,24 @@ type Post interface {
GetPath() string
// Get a string array map with all the post's parameters
GetParameters() map[string][]string
// Get a single parameter array (a parameter can have multiple values)
GetParameter(parameter string) []string
// Get the post section name
GetSection() string
// Get the published date string
GetPublished() string
// Get the updated date string
GetUpdated() string
// Get the post content (markdown)
GetContent() string
}
// RenderContext
type RenderContext interface {
// Get the path of the request
GetPath() string
// Get the URL
GetURL() string
// Get the blog name
GetBlog() string
}

View File

@ -43,3 +43,18 @@ type UI2 interface {
// The document can be used to add or modify HTML.
RenderWithDocument(renderContext RenderContext, doc *goquery.Document)
}
// UISummary plugins get called when rendering the summary on indexes for a post.
type UISummary interface {
// The renderContext provides information such as the path of the request or the blog name.
// The post contains information about the post for which to render the summary.
// The document can be used to add or modify the default HTML.
RenderSummaryForPost(renderContext RenderContext, post Post, doc *goquery.Document)
}
// UIFooter plugins get called when rendering the footer on each HTML page.
type UIFooter interface {
// The renderContext provides information such as the path of the request or the blog name.
// The document can be used to add or modify the default HTML.
RenderFooter(renderContext RenderContext, doc *goquery.Document)
}

View File

@ -47,6 +47,8 @@ func init() {
"SetConfig": reflect.ValueOf((*plugintypes.SetConfig)(nil)),
"UI": reflect.ValueOf((*plugintypes.UI)(nil)),
"UI2": reflect.ValueOf((*plugintypes.UI2)(nil)),
"UIFooter": reflect.ValueOf((*plugintypes.UIFooter)(nil)),
"UISummary": reflect.ValueOf((*plugintypes.UISummary)(nil)),
// interface wrapper definitions
"_App": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_App)(nil)),
@ -59,6 +61,8 @@ func init() {
"_SetConfig": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetConfig)(nil)),
"_UI": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_UI)(nil)),
"_UI2": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_UI2)(nil)),
"_UIFooter": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_UIFooter)(nil)),
"_UISummary": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_UISummary)(nil)),
}
}
@ -149,6 +153,8 @@ func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) Prio() int {
// _go_goblog_app_app_pkgs_plugintypes_Post is an interface wrapper for Post type
type _go_goblog_app_app_pkgs_plugintypes_Post struct {
IValue interface{}
WGetContent func() string
WGetParameter func(parameter string) []string
WGetParameters func() map[string][]string
WGetPath func() string
WGetPublished func() string
@ -156,6 +162,12 @@ type _go_goblog_app_app_pkgs_plugintypes_Post struct {
WGetUpdated func() string
}
func (W _go_goblog_app_app_pkgs_plugintypes_Post) GetContent() string {
return W.WGetContent()
}
func (W _go_goblog_app_app_pkgs_plugintypes_Post) GetParameter(parameter string) []string {
return W.WGetParameter(parameter)
}
func (W _go_goblog_app_app_pkgs_plugintypes_Post) GetParameters() map[string][]string {
return W.WGetParameters()
}
@ -177,6 +189,7 @@ type _go_goblog_app_app_pkgs_plugintypes_RenderContext struct {
IValue interface{}
WGetBlog func() string
WGetPath func() string
WGetURL func() string
}
func (W _go_goblog_app_app_pkgs_plugintypes_RenderContext) GetBlog() string {
@ -185,6 +198,9 @@ func (W _go_goblog_app_app_pkgs_plugintypes_RenderContext) GetBlog() string {
func (W _go_goblog_app_app_pkgs_plugintypes_RenderContext) GetPath() string {
return W.WGetPath()
}
func (W _go_goblog_app_app_pkgs_plugintypes_RenderContext) GetURL() string {
return W.WGetURL()
}
// _go_goblog_app_app_pkgs_plugintypes_SetApp is an interface wrapper for SetApp type
type _go_goblog_app_app_pkgs_plugintypes_SetApp struct {
@ -225,3 +241,23 @@ type _go_goblog_app_app_pkgs_plugintypes_UI2 struct {
func (W _go_goblog_app_app_pkgs_plugintypes_UI2) RenderWithDocument(renderContext plugintypes.RenderContext, doc *goquery.Document) {
W.WRenderWithDocument(renderContext, doc)
}
// _go_goblog_app_app_pkgs_plugintypes_UIFooter is an interface wrapper for UIFooter type
type _go_goblog_app_app_pkgs_plugintypes_UIFooter struct {
IValue interface{}
WRenderFooter func(renderContext plugintypes.RenderContext, doc *goquery.Document)
}
func (W _go_goblog_app_app_pkgs_plugintypes_UIFooter) RenderFooter(renderContext plugintypes.RenderContext, doc *goquery.Document) {
W.WRenderFooter(renderContext, doc)
}
// _go_goblog_app_app_pkgs_plugintypes_UISummary is an interface wrapper for UISummary type
type _go_goblog_app_app_pkgs_plugintypes_UISummary struct {
IValue interface{}
WRenderSummaryForPost func(renderContext plugintypes.RenderContext, post plugintypes.Post, doc *goquery.Document)
}
func (W _go_goblog_app_app_pkgs_plugintypes_UISummary) RenderSummaryForPost(renderContext plugintypes.RenderContext, post plugintypes.Post, doc *goquery.Document) {
W.WRenderSummaryForPost(renderContext, post, doc)
}

View File

@ -22,6 +22,8 @@ const (
pluginUi2Type = "ui2"
pluginExecType = "exec"
pluginMiddlewareType = "middleware"
pluginUiSummaryType = "uisummary"
pluginUiFooterType = "uifooter"
)
func (a *goBlog) initPlugins() error {
@ -37,6 +39,8 @@ func (a *goBlog) initPlugins() error {
pluginUi2Type: reflect.TypeOf((*plugintypes.UI2)(nil)).Elem(),
pluginExecType: reflect.TypeOf((*plugintypes.Exec)(nil)).Elem(),
pluginMiddlewareType: reflect.TypeOf((*plugintypes.Middleware)(nil)).Elem(),
pluginUiSummaryType: reflect.TypeOf((*plugintypes.UISummary)(nil)).Elem(),
pluginUiFooterType: reflect.TypeOf((*plugintypes.UIFooter)(nil)).Elem(),
},
yaegiwrappers.Symbols,
subFS,
@ -106,6 +110,10 @@ func (p *post) GetParameters() map[string][]string {
return p.Parameters
}
func (p *post) GetParameter(parameter string) []string {
return p.Parameters[parameter]
}
func (p *post) GetSection() string {
return p.Section
}
@ -117,3 +125,7 @@ func (p *post) GetPublished() string {
func (p *post) GetUpdated() string {
return p.Updated
}
func (p *post) GetContent() string {
return p.Content
}

View File

@ -22,9 +22,10 @@ func GetPlugin() (
plugintypes.UI2,
plugintypes.Exec,
plugintypes.Middleware,
plugintypes.UISummary,
) {
p := &plugin{}
return p, p, p, p, p, p
return p, p, p, p, p, p, p
}
// SetApp
@ -97,3 +98,8 @@ func (p *plugin) Handler(next http.Handler) http.Handler {
next.ServeHTTP(w, r)
})
}
// UISummary
func (p *plugin) RenderSummaryForPost(rc plugintypes.RenderContext, post plugintypes.Post, doc *goquery.Document) {
doc.Find(".h-entry").AppendHtml(fmt.Sprintf("<p>Summary for post %s on %s</p>", post.GetPath(), rc.GetURL()))
}

View File

@ -9,7 +9,7 @@ import (
"go.goblog.app/app/pkgs/plugintypes"
)
func GetPlugin() (plugintypes.SetConfig, plugintypes.UI2) {
func GetPlugin() (plugintypes.SetConfig, plugintypes.UIFooter) {
p := &plugin{}
return p, p
}
@ -22,7 +22,7 @@ func (p *plugin) SetConfig(config map[string]any) {
p.config = config
}
func (p *plugin) RenderWithDocument(rc plugintypes.RenderContext, doc *goquery.Document) {
func (p *plugin) RenderFooter(rc plugintypes.RenderContext, doc *goquery.Document) {
blog := rc.GetBlog()
if blog == "" {
fmt.Println("webrings plugin: blog is empty!")

View File

@ -20,6 +20,8 @@ type renderData struct {
WebmentionReceivingEnabled bool
TorUsed bool
EasterEgg bool
// For plugins
prc *pluginRenderContext
// Not directly accessible
app *goBlog
req *http.Request
@ -40,36 +42,26 @@ func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, st
w.Header().Set(contentType, contenttype.HTMLUTF8)
// Write status code
w.WriteHeader(statusCode)
// Render
// Render (with UI2 plugins)
renderPipeReader, renderPipeWriter := io.Pipe()
go func() {
f(htmlbuilder.NewHtmlBuilder(renderPipeWriter), data)
hb, finish := a.wrapForPlugins(
renderPipeWriter,
a.getPlugins(pluginUi2Type),
func(plugin any, doc *goquery.Document) {
plugin.(plugintypes.UI2).RenderWithDocument(data.prc, doc)
},
)
f(hb, data)
finish()
_ = renderPipeWriter.Close()
}()
// Create render context for plugins
rc := &pluginRenderContext{
blog: data.BlogString,
path: r.URL.Path,
}
// Run UI plugins
// Run io based UI plugins
pluginPipeReader, pluginPipeWriter := io.Pipe()
go func() {
a.chainUiPlugins(a.getPlugins(pluginUiType), rc, renderPipeReader, pluginPipeWriter)
a.chainUiPlugins(a.getPlugins(pluginUiType), data.prc, renderPipeReader, pluginPipeWriter)
_ = pluginPipeWriter.Close()
}()
// Run UI2 plugins
ui2Plugins := a.getPlugins(pluginUi2Type)
if len(ui2Plugins) > 0 {
doc, _ := goquery.NewDocumentFromReader(pluginPipeReader)
_ = pluginPipeReader.Close()
for _, plugin := range ui2Plugins {
plugin.(plugintypes.UI2).RenderWithDocument(rc, doc)
}
pluginPipeReader, pluginPipeWriter = io.Pipe()
go func() {
_ = pluginPipeWriter.CloseWithError(goquery.Render(pluginPipeWriter, doc.Selection))
}()
}
// Return minified HTML
_ = pluginPipeReader.CloseWithError(a.min.Get().Minify(contenttype.HTML, w, pluginPipeReader))
}
@ -125,6 +117,14 @@ func (a *goBlog) checkRenderData(r *http.Request, data *renderData) {
if ee := a.cfg.EasterEgg; ee != nil && ee.Enabled {
data.EasterEgg = true
}
// Plugins
if data.prc == nil {
data.prc = &pluginRenderContext{
blog: data.BlogString,
path: r.URL.Path,
url: a.getFullAddress(r.URL.Path),
}
}
// Data
if data.Data == nil {
data.Data = map[string]any{}
@ -136,6 +136,7 @@ func (a *goBlog) checkRenderData(r *http.Request, data *renderData) {
type pluginRenderContext struct {
blog string
path string
url string
}
func (d *pluginRenderContext) GetBlog() string {
@ -145,3 +146,7 @@ func (d *pluginRenderContext) GetBlog() string {
func (d *pluginRenderContext) GetPath() string {
return d.path
}
func (d *pluginRenderContext) GetURL() string {
return d.url
}

31
ui.go
View File

@ -140,34 +140,7 @@ func (a *goBlog) renderBase(hb *htmlbuilder.HtmlBuilder, rd *renderData, title,
main(hb)
}
// Footer
hb.WriteElementOpen("footer")
// Footer menu
if fm, ok := rd.Blog.Menus["footer"]; ok {
hb.WriteElementOpen("nav")
for i, item := range fm.Items {
if i > 0 {
hb.WriteUnescaped(" &bull; ")
}
hb.WriteElementOpen("a", "href", item.Link)
hb.WriteEscaped(a.renderMdTitle(item.Title))
hb.WriteElementClose("a")
}
hb.WriteElementClose("nav")
}
// Copyright
hb.WriteElementOpen("p", "translate", "no")
hb.WriteUnescaped("&copy; ")
hb.WriteEscaped(time.Now().Format("2006"))
hb.WriteUnescaped(" ")
if user != nil && user.Name != "" {
hb.WriteEscaped(user.Name)
} else {
hb.WriteEscaped(renderedBlogTitle)
}
hb.WriteElementClose("p")
// Tor
a.renderTorNotice(hb, rd)
hb.WriteElementClose("footer")
a.renderFooter(hb, rd)
// Easter egg
if rd.EasterEgg {
hb.WriteElementOpen("script", "src", a.assetFileName("js/easteregg.js"), "defer", "")
@ -409,7 +382,7 @@ func (a *goBlog) renderIndex(hb *htmlbuilder.HtmlBuilder, rd *renderData) {
if id.posts != nil && len(id.posts) > 0 {
// Posts
for _, p := range id.posts {
a.renderSummary(hb, rd.Blog, p, id.summaryTemplate)
a.renderSummary(hb, rd, rd.Blog, p, id.summaryTemplate)
}
} else {
// No posts

View File

@ -5,8 +5,10 @@ import (
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/samber/lo"
"go.goblog.app/app/pkgs/htmlbuilder"
"go.goblog.app/app/pkgs/plugintypes"
)
type summaryTyp string
@ -17,13 +19,18 @@ const (
)
// post summary on index pages
func (a *goBlog) renderSummary(hb *htmlbuilder.HtmlBuilder, bc *configBlog, p *post, typ summaryTyp) {
func (a *goBlog) renderSummary(origHb *htmlbuilder.HtmlBuilder, rd *renderData, bc *configBlog, p *post, typ summaryTyp) {
if bc == nil || p == nil {
return
}
if typ == "" {
typ = defaultSummary
}
// Plugin handling
hb, finish := a.wrapForPlugins(origHb, a.getPlugins(pluginUiSummaryType), func(plugin any, doc *goquery.Document) {
plugin.(plugintypes.UISummary).RenderSummaryForPost(rd.prc, p, doc)
})
defer finish()
// Start article
hb.WriteElementOpen("article", "class", "h-entry border-bottom")
if p.Priority > 0 {
@ -674,3 +681,40 @@ func (a *goBlog) renderUserSettings(hb *htmlbuilder.HtmlBuilder, rd *renderData,
)
hb.WriteElementClose("form")
}
func (a *goBlog) renderFooter(origHb *htmlbuilder.HtmlBuilder, rd *renderData) {
// Wrap plugins
hb, finish := a.wrapForPlugins(origHb, a.getPlugins(pluginUiFooterType), func(plugin any, doc *goquery.Document) {
plugin.(plugintypes.UIFooter).RenderFooter(rd.prc, doc)
})
defer finish()
// Render footer
hb.WriteElementOpen("footer")
// Footer menu
if fm, ok := rd.Blog.Menus["footer"]; ok {
hb.WriteElementOpen("nav")
for i, item := range fm.Items {
if i > 0 {
hb.WriteUnescaped(" &bull; ")
}
hb.WriteElementOpen("a", "href", item.Link)
hb.WriteEscaped(a.renderMdTitle(item.Title))
hb.WriteElementClose("a")
}
hb.WriteElementClose("nav")
}
// Copyright
hb.WriteElementOpen("p", "translate", "no")
hb.WriteUnescaped("&copy; ")
hb.WriteEscaped(time.Now().Format("2006"))
hb.WriteUnescaped(" ")
if user := a.cfg.User; user != nil && user.Name != "" {
hb.WriteEscaped(user.Name)
} else {
hb.WriteEscaped(a.renderMdTitle(rd.Blog.Title))
}
hb.WriteElementClose("p")
// Tor
a.renderTorNotice(hb, rd)
hb.WriteElementClose("footer")
}

40
uiPluginsHelper.go Normal file
View File

@ -0,0 +1,40 @@
package main
import (
"io"
"sync"
"github.com/PuerkitoBio/goquery"
"go.goblog.app/app/pkgs/htmlbuilder"
)
func (*goBlog) wrapForPlugins(
originalWriter io.Writer,
plugins []any,
pluginRender func(plugin any, doc *goquery.Document),
) (wrappedHb *htmlbuilder.HtmlBuilder, finish func()) {
if len(plugins) == 0 {
// No plugins, nothing to wrap
if hb, ok := (originalWriter).(*htmlbuilder.HtmlBuilder); ok {
return hb, func() {}
}
return htmlbuilder.NewHtmlBuilder(originalWriter), func() {}
}
var wg sync.WaitGroup
pr, pw := io.Pipe()
finish = func() {
_ = pw.Close()
wg.Wait()
}
go func() {
wg.Add(1)
defer wg.Done()
doc, err := goquery.NewDocumentFromReader(pr)
_ = pr.CloseWithError(err)
for _, plugin := range plugins {
pluginRender(plugin, doc)
}
_ = goquery.Render(originalWriter, doc.Selection)
}()
return htmlbuilder.NewHtmlBuilder(pw), finish
}