From 8c9d17006de0ccccacf4e7bae203650d3a61923d Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Fri, 10 Mar 2023 15:14:50 +0100 Subject: [PATCH] New plugin types: UISummary and UIFooter --- docs/plugins.md | 14 +++--- pkgs/plugintypes/app.go | 6 +++ pkgs/plugintypes/plugins.go | 15 ++++++ .../go_goblog_app-app-pkgs-plugintypes.go | 36 ++++++++++++++ plugins.go | 12 +++++ plugins/demo/src/demo/demo.go | 8 ++- plugins/webrings/src/webrings/webrings.go | 4 +- render.go | 49 ++++++++++--------- ui.go | 31 +----------- uiComponents.go | 46 ++++++++++++++++- uiPluginsHelper.go | 40 +++++++++++++++ 11 files changed, 200 insertions(+), 61 deletions(-) create mode 100644 uiPluginsHelper.go diff --git a/docs/plugins.md b/docs/plugins.md index e7ade83..f3f8271 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -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. diff --git a/pkgs/plugintypes/app.go b/pkgs/plugintypes/app.go index 9ac5198..4117fa0 100644 --- a/pkgs/plugintypes/app.go +++ b/pkgs/plugintypes/app.go @@ -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 } diff --git a/pkgs/plugintypes/plugins.go b/pkgs/plugintypes/plugins.go index 809675d..0aaa73e 100644 --- a/pkgs/plugintypes/plugins.go +++ b/pkgs/plugintypes/plugins.go @@ -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) +} diff --git a/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go b/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go index 2ca57e6..3cc5324 100644 --- a/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go +++ b/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go @@ -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) +} diff --git a/plugins.go b/plugins.go index b16f1ee..a050c04 100644 --- a/plugins.go +++ b/plugins.go @@ -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 +} diff --git a/plugins/demo/src/demo/demo.go b/plugins/demo/src/demo/demo.go index 2d56e92..15e48a9 100644 --- a/plugins/demo/src/demo/demo.go +++ b/plugins/demo/src/demo/demo.go @@ -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("

Summary for post %s on %s

", post.GetPath(), rc.GetURL())) +} diff --git a/plugins/webrings/src/webrings/webrings.go b/plugins/webrings/src/webrings/webrings.go index 95bceed..667c39b 100644 --- a/plugins/webrings/src/webrings/webrings.go +++ b/plugins/webrings/src/webrings/webrings.go @@ -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!") diff --git a/render.go b/render.go index a722e66..20cc7e1 100644 --- a/render.go +++ b/render.go @@ -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 +} diff --git a/ui.go b/ui.go index c50c14a..73e554e 100644 --- a/ui.go +++ b/ui.go @@ -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(" • ") - } - 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("© ") - 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 diff --git a/uiComponents.go b/uiComponents.go index 68c79e8..8e4e31d 100644 --- a/uiComponents.go +++ b/uiComponents.go @@ -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(" • ") + } + 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("© ") + 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") +} diff --git a/uiPluginsHelper.go b/uiPluginsHelper.go new file mode 100644 index 0000000..521dbe7 --- /dev/null +++ b/uiPluginsHelper.go @@ -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 +}