From a45d28d04fba75da37a417fcff43e5b0fb29c6e8 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Fri, 12 Aug 2022 12:48:16 +0200 Subject: [PATCH] Add UI plugins (#33) and improve documentation for plugins (#32) --- docs/index.md | 3 +- docs/plugins.md | 44 + editor.go | 3 +- go.mod | 2 +- go.sum | 4 +- http.go | 2 +- markdown.go | 9 +- pkgs/htmlbuilder/htmlbuilder.go | 72 + pkgs/htmlbuilder/htmlbuilder_test.go | 6 + pkgs/plugintypes/{types.go => goblog.go} | 43 +- pkgs/plugintypes/plugins.go | 38 + .../go_goblog_app-app-pkgs-htmlbuilder.go | 40 + .../go_goblog_app-app-pkgs-plugintypes.go | 81 +- pkgs/yaegiwrappers/wrappers.go | 1 + plugins.go | 36 +- plugins/demo/src/demoui/demo.go | 36 + .../src/syndication/syndication.go | 54 + plugins_test.go | 5 + postsFuncs.go | 43 +- render.go | 7 +- ui.go | 1485 +++++++++-------- uiComponents.go | 609 +++---- uiHtmlBuilder.go | 72 - ui_test.go | 13 +- 24 files changed, 1519 insertions(+), 1189 deletions(-) create mode 100644 docs/plugins.md create mode 100644 pkgs/htmlbuilder/htmlbuilder.go create mode 100644 pkgs/htmlbuilder/htmlbuilder_test.go rename pkgs/plugintypes/{types.go => goblog.go} (53%) create mode 100644 pkgs/plugintypes/plugins.go create mode 100644 pkgs/yaegiwrappers/go_goblog_app-app-pkgs-htmlbuilder.go create mode 100644 plugins/demo/src/demoui/demo.go create mode 100644 plugins/syndication/src/syndication/syndication.go delete mode 100644 uiHtmlBuilder.go diff --git a/docs/index.md b/docs/index.md index c891504..0f8231a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -58,4 +58,5 @@ Here's an (incomplete) list of features: - [How to use GoBlog](./usage.md) - [How to configure GoBlog](./config.md) - [Administration paths](./admin-paths.md) -- [GoBlog's storage system](./storage.md) \ No newline at end of file +- [GoBlog's storage system](./storage.md) +- [GoBlog Plugins](./plugins.md) \ No newline at end of file diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 0000000..ebcb781 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,44 @@ +# GoBlog Plugins + +GoBlog has a (still experimental) plugin system, that allows adding new functionality to GoBlog without adding anything to the GoBlog source and recompiling GoBlog. Plugins work using the [Yaegi](https://github.com/traefik/yaegi) package by Traefik and are interpreted at run time. + +## Configuration + +Plugins can be added to GoBlog by adding a "plugins" section to the configuration. + +```yaml +plugins: + - path: ./plugins/syndication + type: ui + import: syndication + config: + parameter: syndication + - path: ./plugins/demo + type: ui + import: demoui + - path: ./plugins/demo + type: middleware + import: demomiddleware + config: + prio: 99 +``` + +You need to specify the path to the plugin (remember to mount the path to your GoBlog container when using Docker), the type of the plugin, the import (the Go packakge) and you can additionally provide configuration for the plugin. + +## Types of plugins + +- `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` (Render additional HTML) - see https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#UI + +## Plugins + +Some simple plugins are included in the main GoBlog repository. Some can be found elsewhere. + +### Syndication links (plugins/syndication) + +Adds hidden `u-syndication` `data` elements to post page when the configured post parameter (default: "syndication") is available. + +#### Config + +`parameter` (string): Name for the post parameter containing the syndication links. \ No newline at end of file diff --git a/editor.go b/editor.go index 23488e5..f707b2a 100644 --- a/editor.go +++ b/editor.go @@ -12,6 +12,7 @@ import ( "go.goblog.app/app/pkgs/bufferpool" "go.goblog.app/app/pkgs/contenttype" + "go.goblog.app/app/pkgs/htmlbuilder" "gopkg.in/yaml.v3" ws "nhooyr.io/websocket" ) @@ -77,7 +78,7 @@ func (a *goBlog) createMarkdownPreview(w io.Writer, blog string, markdown io.Rea p.RenderedTitle = a.renderMdTitle(t) } // Render post (using post's blog config) - hb := newHtmlBuilder(w) + hb := htmlbuilder.NewHtmlBuilder(w) a.renderEditorPreview(hb, a.cfg.Blogs[p.Blog], p) } diff --git a/go.mod b/go.mod index 4ad011f..f100d0f 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( // master github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa - golang.org/x/net v0.0.0-20220805013720-a33c5aa5df48 + golang.org/x/net v0.0.0-20220811182439-13a9a731de15 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 golang.org/x/text v0.3.7 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 0c86d20..9c81656 100644 --- a/go.sum +++ b/go.sum @@ -619,8 +619,8 @@ golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220805013720-a33c5aa5df48 h1:N9Vc/rorQUDes6B9CNdIxAn5jODGj2wzfrei2x4wNj4= -golang.org/x/net v0.0.0-20220805013720-a33c5aa5df48/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220811182439-13a9a731de15 h1:cik0bxZUSJVDyaHf1hZPSDsU8SZHGQZQMeueXCE7yBQ= +golang.org/x/net v0.0.0-20220811182439-13a9a731de15/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/http.go b/http.go index 02582d0..2120966 100644 --- a/http.go +++ b/http.go @@ -46,7 +46,7 @@ func (a *goBlog) startServer() (err error) { h = h.Append(a.securityHeaders) } // Add plugin middlewares - middlewarePlugins := getPluginsForType[plugintypes.Middleware](a, "middleware") + middlewarePlugins := getPluginsForType[plugintypes.Middleware](a, middlewarePlugin) sort.Slice(middlewarePlugins, func(i, j int) bool { // Sort with descending prio return middlewarePlugins[i].Prio() > middlewarePlugins[j].Prio() diff --git a/markdown.go b/markdown.go index 39456b1..bbaf1fd 100644 --- a/markdown.go +++ b/markdown.go @@ -15,6 +15,7 @@ import ( "github.com/yuin/goldmark/util" "go.goblog.app/app/pkgs/bufferpool" "go.goblog.app/app/pkgs/highlighting" + "go.goblog.app/app/pkgs/htmlbuilder" ) func (a *goBlog) initMarkdown() { @@ -183,13 +184,13 @@ func (c *customRenderer) renderImage(w util.BufWriter, source []byte, node ast.N dest = resolved[0] } } - hb := newHtmlBuilder(w) - hb.writeElementOpen("a", "href", dest) + hb := htmlbuilder.NewHtmlBuilder(w) + hb.WriteElementOpen("a", "href", dest) imgEls := []any{"src", dest, "alt", string(n.Text(source)), "loading", "lazy"} if len(n.Title) > 0 { imgEls = append(imgEls, "title", string(n.Title)) } - hb.writeElementOpen("img", imgEls...) - hb.writeElementClose("a") + hb.WriteElementOpen("img", imgEls...) + hb.WriteElementClose("a") return ast.WalkSkipChildren, nil } diff --git a/pkgs/htmlbuilder/htmlbuilder.go b/pkgs/htmlbuilder/htmlbuilder.go new file mode 100644 index 0000000..63cc90e --- /dev/null +++ b/pkgs/htmlbuilder/htmlbuilder.go @@ -0,0 +1,72 @@ +package htmlbuilder + +import ( + "fmt" + "io" + textTemplate "text/template" +) + +type HtmlBuilder struct { + w io.Writer +} + +func NewHtmlBuilder(w io.Writer) *HtmlBuilder { + return &HtmlBuilder{ + w: w, + } +} + +func (h *HtmlBuilder) getWriter() io.Writer { + return h.w +} + +func (h *HtmlBuilder) Write(p []byte) (int, error) { + return h.getWriter().Write(p) +} + +func (h *HtmlBuilder) WriteString(s string) (int, error) { + return io.WriteString(h.getWriter(), s) +} + +func (h *HtmlBuilder) WriteUnescaped(s string) { + _, _ = h.WriteString(s) +} + +func (h *HtmlBuilder) WriteEscaped(s string) { + textTemplate.HTMLEscape(h, []byte(s)) +} + +func (h *HtmlBuilder) WriteAttribute(attr string, val any) { + h.WriteUnescaped(` `) + h.WriteUnescaped(attr) + h.WriteUnescaped(`=`) + if valStr, ok := val.(string); ok { + h.WriteUnescaped(`"`) + h.WriteEscaped(valStr) + h.WriteUnescaped(`"`) + } else { + h.WriteEscaped(fmt.Sprint(val)) + } +} + +func (h *HtmlBuilder) WriteElementOpen(tag string, attrs ...any) { + h.WriteUnescaped(`<`) + h.WriteUnescaped(tag) + for i := 0; i < len(attrs); i += 2 { + if i+2 > len(attrs) { + break + } + attrStr, ok := attrs[i].(string) + if !ok { + continue + } + h.WriteAttribute(attrStr, attrs[i+1]) + } + h.WriteUnescaped(`>`) +} + +func (h *HtmlBuilder) WriteElementClose(tag string) { + h.WriteUnescaped(``) +} diff --git a/pkgs/htmlbuilder/htmlbuilder_test.go b/pkgs/htmlbuilder/htmlbuilder_test.go new file mode 100644 index 0000000..17199a2 --- /dev/null +++ b/pkgs/htmlbuilder/htmlbuilder_test.go @@ -0,0 +1,6 @@ +package htmlbuilder + +import "io" + +var _ io.Writer = &HtmlBuilder{} +var _ io.StringWriter = &HtmlBuilder{} diff --git a/pkgs/plugintypes/types.go b/pkgs/plugintypes/goblog.go similarity index 53% rename from pkgs/plugintypes/types.go rename to pkgs/plugintypes/goblog.go index 0e51fea..8cb8c81 100644 --- a/pkgs/plugintypes/types.go +++ b/pkgs/plugintypes/goblog.go @@ -3,10 +3,9 @@ package plugintypes import ( "context" "database/sql" - "net/http" -) -// Interface to GoBlog + "go.goblog.app/app/pkgs/htmlbuilder" +) // App is used to access GoBlog's app instance. type App interface { @@ -23,29 +22,27 @@ type Database interface { QueryRowContext(context.Context, string, ...any) (*sql.Row, error) } -// Plugin types - -// SetApp is used in all plugin types to allow -// GoBlog set it's app instance to be accessible by the plugin. -type SetApp interface { - SetApp(App) +// Post +type Post interface { + GetParameters() map[string][]string } -// SetConfig is used in all plugin types to allow -// GoBlog set plugin configuration. -type SetConfig interface { - SetConfig(map[string]any) +// RenderType +type RenderType string + +// RenderData +type RenderData interface { + // Empty } -type Exec interface { - SetApp - SetConfig - Exec() -} +// RenderNextFunc +type RenderNextFunc func(*htmlbuilder.HtmlBuilder) -type Middleware interface { - SetApp - SetConfig - Handler(http.Handler) http.Handler - Prio() int +// Render main element content on post page, data = PostRenderData +const PostMainElementRenderType RenderType = "post-main-content" + +// PostRenderData is RenderData containing a Post +type PostRenderData interface { + RenderData + GetPost() Post } diff --git a/pkgs/plugintypes/plugins.go b/pkgs/plugintypes/plugins.go new file mode 100644 index 0000000..32740f2 --- /dev/null +++ b/pkgs/plugintypes/plugins.go @@ -0,0 +1,38 @@ +package plugintypes + +import ( + "net/http" + + "go.goblog.app/app/pkgs/htmlbuilder" +) + +// SetApp is used in all plugin types to allow +// GoBlog set it's app instance to be accessible by the plugin. +type SetApp interface { + SetApp(App) +} + +// SetConfig is used in all plugin types to allow +// GoBlog set plugin configuration. +type SetConfig interface { + SetConfig(map[string]any) +} + +type Exec interface { + SetApp + SetConfig + Exec() +} + +type Middleware interface { + SetApp + SetConfig + Handler(http.Handler) http.Handler + Prio() int +} + +type UI interface { + SetApp + SetConfig + Render(*htmlbuilder.HtmlBuilder, RenderType, RenderData, RenderNextFunc) +} diff --git a/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-htmlbuilder.go b/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-htmlbuilder.go new file mode 100644 index 0000000..f13e699 --- /dev/null +++ b/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-htmlbuilder.go @@ -0,0 +1,40 @@ +// Code generated by 'yaegi extract go.goblog.app/app/pkgs/htmlbuilder'. DO NOT EDIT. + +// MIT License +// +// Copyright (c) 2020 - 2022 Jan-Lukas Else +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package yaegiwrappers + +import ( + "go.goblog.app/app/pkgs/htmlbuilder" + "reflect" +) + +func init() { + Symbols["go.goblog.app/app/pkgs/htmlbuilder/htmlbuilder"] = map[string]reflect.Value{ + // function, constant and variable definitions + "NewHtmlBuilder": reflect.ValueOf(htmlbuilder.NewHtmlBuilder), + + // type definitions + "HtmlBuilder": reflect.ValueOf((*htmlbuilder.HtmlBuilder)(nil)), + } +} diff --git a/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go b/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go index 07ab411..29980f5 100644 --- a/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go +++ b/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go @@ -27,6 +27,7 @@ package yaegiwrappers import ( "context" "database/sql" + "go.goblog.app/app/pkgs/htmlbuilder" "go.goblog.app/app/pkgs/plugintypes" "net/http" "reflect" @@ -34,21 +35,34 @@ import ( func init() { Symbols["go.goblog.app/app/pkgs/plugintypes/plugintypes"] = map[string]reflect.Value{ + // function, constant and variable definitions + "PostMainElementRenderType": reflect.ValueOf(plugintypes.PostMainElementRenderType), + // type definitions - "App": reflect.ValueOf((*plugintypes.App)(nil)), - "Database": reflect.ValueOf((*plugintypes.Database)(nil)), - "Exec": reflect.ValueOf((*plugintypes.Exec)(nil)), - "Middleware": reflect.ValueOf((*plugintypes.Middleware)(nil)), - "SetApp": reflect.ValueOf((*plugintypes.SetApp)(nil)), - "SetConfig": reflect.ValueOf((*plugintypes.SetConfig)(nil)), + "App": reflect.ValueOf((*plugintypes.App)(nil)), + "Database": reflect.ValueOf((*plugintypes.Database)(nil)), + "Exec": reflect.ValueOf((*plugintypes.Exec)(nil)), + "Middleware": reflect.ValueOf((*plugintypes.Middleware)(nil)), + "Post": reflect.ValueOf((*plugintypes.Post)(nil)), + "PostRenderData": reflect.ValueOf((*plugintypes.PostRenderData)(nil)), + "RenderData": reflect.ValueOf((*plugintypes.RenderData)(nil)), + "RenderNextFunc": reflect.ValueOf((*plugintypes.RenderNextFunc)(nil)), + "RenderType": reflect.ValueOf((*plugintypes.RenderType)(nil)), + "SetApp": reflect.ValueOf((*plugintypes.SetApp)(nil)), + "SetConfig": reflect.ValueOf((*plugintypes.SetConfig)(nil)), + "UI": reflect.ValueOf((*plugintypes.UI)(nil)), // interface wrapper definitions - "_App": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_App)(nil)), - "_Database": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Database)(nil)), - "_Exec": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Exec)(nil)), - "_Middleware": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Middleware)(nil)), - "_SetApp": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetApp)(nil)), - "_SetConfig": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetConfig)(nil)), + "_App": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_App)(nil)), + "_Database": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Database)(nil)), + "_Exec": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Exec)(nil)), + "_Middleware": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Middleware)(nil)), + "_Post": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Post)(nil)), + "_PostRenderData": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_PostRenderData)(nil)), + "_RenderData": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_RenderData)(nil)), + "_SetApp": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetApp)(nil)), + "_SetConfig": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetConfig)(nil)), + "_UI": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_UI)(nil)), } } @@ -132,6 +146,31 @@ func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) SetConfig(a0 map[string] W.WSetConfig(a0) } +// _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{} + WGetParameters func() map[string][]string +} + +func (W _go_goblog_app_app_pkgs_plugintypes_Post) GetParameters() map[string][]string { + return W.WGetParameters() +} + +// _go_goblog_app_app_pkgs_plugintypes_PostRenderData is an interface wrapper for PostRenderData type +type _go_goblog_app_app_pkgs_plugintypes_PostRenderData struct { + IValue interface{} + WGetPost func() plugintypes.Post +} + +func (W _go_goblog_app_app_pkgs_plugintypes_PostRenderData) GetPost() plugintypes.Post { + return W.WGetPost() +} + +// _go_goblog_app_app_pkgs_plugintypes_RenderData is an interface wrapper for RenderData type +type _go_goblog_app_app_pkgs_plugintypes_RenderData struct { + IValue interface{} +} + // _go_goblog_app_app_pkgs_plugintypes_SetApp is an interface wrapper for SetApp type type _go_goblog_app_app_pkgs_plugintypes_SetApp struct { IValue interface{} @@ -151,3 +190,21 @@ type _go_goblog_app_app_pkgs_plugintypes_SetConfig struct { func (W _go_goblog_app_app_pkgs_plugintypes_SetConfig) SetConfig(a0 map[string]any) { W.WSetConfig(a0) } + +// _go_goblog_app_app_pkgs_plugintypes_UI is an interface wrapper for UI type +type _go_goblog_app_app_pkgs_plugintypes_UI struct { + IValue interface{} + WRender func(a0 *htmlbuilder.HtmlBuilder, a1 plugintypes.RenderType, a2 plugintypes.RenderData, a3 plugintypes.RenderNextFunc) + WSetApp func(a0 plugintypes.App) + WSetConfig func(a0 map[string]any) +} + +func (W _go_goblog_app_app_pkgs_plugintypes_UI) Render(a0 *htmlbuilder.HtmlBuilder, a1 plugintypes.RenderType, a2 plugintypes.RenderData, a3 plugintypes.RenderNextFunc) { + W.WRender(a0, a1, a2, a3) +} +func (W _go_goblog_app_app_pkgs_plugintypes_UI) SetApp(a0 plugintypes.App) { + W.WSetApp(a0) +} +func (W _go_goblog_app_app_pkgs_plugintypes_UI) SetConfig(a0 map[string]any) { + W.WSetConfig(a0) +} diff --git a/pkgs/yaegiwrappers/wrappers.go b/pkgs/yaegiwrappers/wrappers.go index b6d2c7d..5b6919e 100644 --- a/pkgs/yaegiwrappers/wrappers.go +++ b/pkgs/yaegiwrappers/wrappers.go @@ -9,3 +9,4 @@ var ( ) //go:generate yaegi extract -license ../../LICENSE -name yaegiwrappers go.goblog.app/app/pkgs/plugintypes +//go:generate yaegi extract -license ../../LICENSE -name yaegiwrappers go.goblog.app/app/pkgs/htmlbuilder diff --git a/plugins.go b/plugins.go index 3ff5cb9..c6460e2 100644 --- a/plugins.go +++ b/plugins.go @@ -6,11 +6,18 @@ import ( "go.goblog.app/app/pkgs/yaegiwrappers" ) +const ( + execPlugin = "exec" + middlewarePlugin = "middleware" + uiPlugin = "ui" +) + func (a *goBlog) initPlugins() error { a.pluginHost = plugins.NewPluginHost(yaegiwrappers.Symbols) - a.pluginHost.AddPluginType("exec", (*plugintypes.Exec)(nil)) - a.pluginHost.AddPluginType("middleware", (*plugintypes.Middleware)(nil)) + a.pluginHost.AddPluginType(execPlugin, (*plugintypes.Exec)(nil)) + a.pluginHost.AddPluginType(middlewarePlugin, (*plugintypes.Middleware)(nil)) + a.pluginHost.AddPluginType(uiPlugin, (*plugintypes.UI)(nil)) for _, pc := range a.cfg.Plugins { if pluginInterface, err := a.pluginHost.LoadPlugin(&plugins.PluginConfig{ @@ -29,7 +36,7 @@ func (a *goBlog) initPlugins() error { } } - execs := getPluginsForType[plugintypes.Exec](a, "exec") + execs := getPluginsForType[plugintypes.Exec](a, execPlugin) for _, p := range execs { go p.Exec() } @@ -38,15 +45,30 @@ func (a *goBlog) initPlugins() error { } func getPluginsForType[T any](a *goBlog, pluginType string) (list []T) { + if a == nil || a.pluginHost == nil { + return nil + } return plugins.GetPluginsForType[T](a.pluginHost, pluginType) } -// Implement all needed interfaces for goblog - -var _ plugintypes.App = &goBlog{} +// Implement all needed interfaces func (a *goBlog) GetDatabase() plugintypes.Database { return a.db } -var _ plugintypes.Database = &database{} +func (p *post) GetParameters() map[string][]string { + return p.Parameters +} + +type pluginPostRenderData struct { + p *post +} + +func (d *pluginPostRenderData) GetPost() plugintypes.Post { + return d.p +} + +func (p *post) pluginRenderData() plugintypes.PostRenderData { + return &pluginPostRenderData{p: p} +} diff --git a/plugins/demo/src/demoui/demo.go b/plugins/demo/src/demoui/demo.go new file mode 100644 index 0000000..e6ecc44 --- /dev/null +++ b/plugins/demo/src/demoui/demo.go @@ -0,0 +1,36 @@ +package demoui + +import ( + "go.goblog.app/app/pkgs/htmlbuilder" + "go.goblog.app/app/pkgs/plugintypes" +) + +func GetPlugin() plugintypes.UI { + return &plugin{} +} + +type plugin struct{} + +func (*plugin) SetApp(_ plugintypes.App) { + // Ignore +} + +func (*plugin) SetConfig(_ map[string]any) { + // Ignore +} + +func (*plugin) Render(hb *htmlbuilder.HtmlBuilder, t plugintypes.RenderType, _ plugintypes.RenderData, render plugintypes.RenderNextFunc) { + switch t { + case plugintypes.PostMainElementRenderType: + hb.WriteElementOpen("p") + hb.WriteEscaped("Start of post main element") + hb.WriteElementClose("p") + render(hb) + hb.WriteElementOpen("p") + hb.WriteEscaped("End of post main element") + hb.WriteElementClose("p") + return + default: + render(hb) + } +} diff --git a/plugins/syndication/src/syndication/syndication.go b/plugins/syndication/src/syndication/syndication.go new file mode 100644 index 0000000..bb5c34f --- /dev/null +++ b/plugins/syndication/src/syndication/syndication.go @@ -0,0 +1,54 @@ +package syndication + +import ( + "fmt" + + "go.goblog.app/app/pkgs/htmlbuilder" + "go.goblog.app/app/pkgs/plugintypes" +) + +func GetPlugin() plugintypes.UI { + return &plugin{} +} + +type plugin struct { + config map[string]any +} + +func (*plugin) SetApp(_ plugintypes.App) { + // Ignore +} + +func (p *plugin) SetConfig(config map[string]any) { + p.config = config +} + +func (p *plugin) Render(hb *htmlbuilder.HtmlBuilder, t plugintypes.RenderType, data plugintypes.RenderData, render plugintypes.RenderNextFunc) { + switch t { + case plugintypes.PostMainElementRenderType: + render(hb) + pd, ok := data.(plugintypes.PostRenderData) + if !ok { + fmt.Println("syndication plugin: data is not PostRenderData!") + return + } + parameterName := "syndication" // default + if configParameterAny, ok := p.config["parameter"]; ok { + if configParameter, ok := configParameterAny.(string); ok { + parameterName = configParameter // override default from config + } + } + syndicationLinks, ok := pd.GetPost().GetParameters()[parameterName] + if !ok || len(syndicationLinks) == 0 { + // No syndication links + return + } + for _, link := range syndicationLinks { + hb.WriteElementOpen("data", "value", link, "class", "u-syndication hide") + hb.WriteElementClose("data") + } + return + default: + render(hb) + } +} diff --git a/plugins_test.go b/plugins_test.go index d945ad6..f080813 100644 --- a/plugins_test.go +++ b/plugins_test.go @@ -8,6 +8,11 @@ import ( "go.goblog.app/app/pkgs/plugintypes" ) +var _ plugintypes.App = &goBlog{} +var _ plugintypes.Database = &database{} +var _ plugintypes.Post = &post{} +var _ plugintypes.PostRenderData = &pluginPostRenderData{} + func TestExecPlugin(t *testing.T) { app := &goBlog{ cfg: createDefaultTestConfig(t), diff --git a/postsFuncs.go b/postsFuncs.go index 5196322..fa495f3 100644 --- a/postsFuncs.go +++ b/postsFuncs.go @@ -9,6 +9,7 @@ import ( gogeouri "git.jlel.se/jlelse/go-geouri" "github.com/araddon/dateparse" "go.goblog.app/app/pkgs/bufferpool" + "go.goblog.app/app/pkgs/htmlbuilder" "gopkg.in/yaml.v3" ) @@ -44,34 +45,34 @@ func (a *goBlog) postHtml(p *post, absolute bool) (res string) { func (a *goBlog) postHtmlToWriter(w io.Writer, p *post, absolute bool) { // Build HTML - hb := newHtmlBuilder(w) + hb := htmlbuilder.NewHtmlBuilder(w) // Add audio to the top for _, a := range p.Parameters[a.cfg.Micropub.AudioParam] { - hb.writeElementOpen("audio", "controls", "preload", "none") - hb.writeElementOpen("source", "src", a) - hb.writeElementClose("source") - hb.writeElementClose("audio") + hb.WriteElementOpen("audio", "controls", "preload", "none") + hb.WriteElementOpen("source", "src", a) + hb.WriteElementClose("source") + hb.WriteElementClose("audio") } // Render markdown _ = a.renderMarkdownToWriter(w, p.Content, absolute) // Add bookmark links to the bottom for _, l := range p.Parameters[a.cfg.Micropub.BookmarkParam] { - hb.writeElementOpen("p") - hb.writeElementOpen("a", "class", "u-bookmark-of", "href", l, "target", "_blank", "rel", "noopener noreferrer") - hb.writeEscaped(l) - hb.writeElementClose("a") - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteElementOpen("a", "class", "u-bookmark-of", "href", l, "target", "_blank", "rel", "noopener noreferrer") + hb.WriteEscaped(l) + hb.WriteElementClose("a") + hb.WriteElementClose("p") } } func (a *goBlog) feedHtml(w io.Writer, p *post) { - hb := newHtmlBuilder(w) + hb := htmlbuilder.NewHtmlBuilder(w) // Add TTS audio to the top for _, a := range p.Parameters[ttsParameter] { - hb.writeElementOpen("audio", "controls", "preload", "none") - hb.writeElementOpen("source", "src", a) - hb.writeElementClose("source") - hb.writeElementClose("audio") + hb.WriteElementOpen("audio", "controls", "preload", "none") + hb.WriteElementOpen("source", "src", a) + hb.WriteElementClose("source") + hb.WriteElementClose("audio") } // Add IndieWeb context a.renderPostReplyContext(hb, p, "p") @@ -81,16 +82,16 @@ func (a *goBlog) feedHtml(w io.Writer, p *post) { // Add link to interactions and comments blogConfig := a.getBlogFromPost(p) if cc := blogConfig.Comments; cc != nil && cc.Enabled { - hb.writeElementOpen("p") - hb.writeElementOpen("a", "href", a.getFullAddress(p.Path)+"#interactions") - hb.writeEscaped(a.ts.GetTemplateStringVariant(blogConfig.Lang, "interactions")) - hb.writeElementClose("a") - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteElementOpen("a", "href", a.getFullAddress(p.Path)+"#interactions") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(blogConfig.Lang, "interactions")) + hb.WriteElementClose("a") + hb.WriteElementClose("p") } } func (a *goBlog) minFeedHtml(w io.Writer, p *post) { - hb := newHtmlBuilder(w) + hb := htmlbuilder.NewHtmlBuilder(w) // Add IndieWeb context a.renderPostReplyContext(hb, p, "p") a.renderPostLikeContext(hb, p, "p") diff --git a/render.go b/render.go index 0b68979..7aae7f4 100644 --- a/render.go +++ b/render.go @@ -5,6 +5,7 @@ import ( "net/http" "go.goblog.app/app/pkgs/contenttype" + "go.goblog.app/app/pkgs/htmlbuilder" ) type renderData struct { @@ -27,11 +28,11 @@ func (d *renderData) LoggedIn() bool { return d.app.isLoggedIn(d.req) } -func (a *goBlog) render(w http.ResponseWriter, r *http.Request, f func(*htmlBuilder, *renderData), data *renderData) { +func (a *goBlog) render(w http.ResponseWriter, r *http.Request, f func(*htmlbuilder.HtmlBuilder, *renderData), data *renderData) { a.renderWithStatusCode(w, r, http.StatusOK, f, data) } -func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, statusCode int, f func(*htmlBuilder, *renderData), data *renderData) { +func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, statusCode int, f func(*htmlbuilder.HtmlBuilder, *renderData), data *renderData) { // Check render data a.checkRenderData(r, data) // Set content type @@ -41,7 +42,7 @@ func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, st // Render pipeReader, pipeWriter := io.Pipe() go func() { - f(newHtmlBuilder(pipeWriter), data) + f(htmlbuilder.NewHtmlBuilder(pipeWriter), data) _ = pipeWriter.Close() }() _ = pipeReader.CloseWithError(a.min.Get().Minify(contenttype.HTML, w, pipeReader)) diff --git a/ui.go b/ui.go index 4d61a2e..0a663ae 100644 --- a/ui.go +++ b/ui.go @@ -8,31 +8,55 @@ import ( "github.com/kaorimatz/go-opml" "github.com/mergestat/timediff" "github.com/samber/lo" + "go.goblog.app/app/pkgs/htmlbuilder" + "go.goblog.app/app/pkgs/plugintypes" ) -func (a *goBlog) renderEditorPreview(hb *htmlBuilder, bc *configBlog, p *post) { +func (a *goBlog) renderWithPlugins(hb *htmlbuilder.HtmlBuilder, t plugintypes.RenderType, d plugintypes.RenderData, r plugintypes.RenderNextFunc) { + plugins := getPluginsForType[plugintypes.UI](a, uiPlugin) + if len(plugins) == 0 { + r(hb) + return + } + // Reverse plugins, so that the first one in the configuration is executed first + plugins = lo.Reverse(plugins) + plugins[0].Render(hb, t, d, a.wrapUiPlugins(t, d, r, plugins[1:]...)) +} + +func (a *goBlog) wrapUiPlugins(t plugintypes.RenderType, d plugintypes.RenderData, r plugintypes.RenderNextFunc, plugins ...plugintypes.UI) plugintypes.RenderNextFunc { + if len(plugins) == 0 { + // Last element in the chain + return r + } + return func(newHb *htmlbuilder.HtmlBuilder) { + // Wrap the next plugin + plugins[0].Render(newHb, t, d, a.wrapUiPlugins(t, d, r, plugins[1:]...)) + } +} + +func (a *goBlog) renderEditorPreview(hb *htmlbuilder.HtmlBuilder, bc *configBlog, p *post) { a.renderPostTitle(hb, p) a.renderPostMeta(hb, p, bc, "preview") if p.Content != "" { - hb.writeElementOpen("div") + hb.WriteElementOpen("div") a.postHtmlToWriter(hb, p, true) - hb.writeElementClose("div") + hb.WriteElementClose("div") } // a.renderPostGPX(hb, p, bc) a.renderPostTax(hb, p, bc) } -func (a *goBlog) renderBase(hb *htmlBuilder, rd *renderData, title, main func(hb *htmlBuilder)) { +func (a *goBlog) renderBase(hb *htmlbuilder.HtmlBuilder, rd *renderData, title, main func(hb *htmlbuilder.HtmlBuilder)) { // Basic HTML things - hb.write("") - hb.writeElementOpen("html", "lang", rd.Blog.Lang) - hb.writeElementOpen("meta", "charset", "utf-8") - hb.writeElementOpen("meta", "name", "viewport", "content", "width=device-width,initial-scale=1") + hb.WriteUnescaped("") + hb.WriteElementOpen("html", "lang", rd.Blog.Lang) + hb.WriteElementOpen("meta", "charset", "utf-8") + hb.WriteElementOpen("meta", "name", "viewport", "content", "width=device-width,initial-scale=1") // CSS - hb.writeElementOpen("link", "rel", "stylesheet", "href", a.assetFileName("css/styles.css")) + hb.WriteElementOpen("link", "rel", "stylesheet", "href", a.assetFileName("css/styles.css")) // Canonical URL if rd.Canonical != "" { - hb.writeElementOpen("link", "rel", "canonical", "href", rd.Canonical) + hb.WriteElementOpen("link", "rel", "canonical", "href", rd.Canonical) } // Title if title != nil { @@ -42,135 +66,135 @@ func (a *goBlog) renderBase(hb *htmlBuilder, rd *renderData, title, main func(hb } renderedBlogTitle := a.renderMdTitle(rd.Blog.Title) // Feeds - hb.writeElementOpen("link", "rel", "alternate", "type", "application/rss+xml", "title", fmt.Sprintf("RSS (%s)", renderedBlogTitle), "href", a.getFullAddress(rd.Blog.Path+".rss")) - hb.writeElementOpen("link", "rel", "alternate", "type", "application/atom+xml", "title", fmt.Sprintf("ATOM (%s)", renderedBlogTitle), "href", a.getFullAddress(rd.Blog.Path+".atom")) - hb.writeElementOpen("link", "rel", "alternate", "type", "application/feed+json", "title", fmt.Sprintf("JSON Feed (%s)", renderedBlogTitle), "href", a.getFullAddress(rd.Blog.Path+".json")) + hb.WriteElementOpen("link", "rel", "alternate", "type", "application/rss+xml", "title", fmt.Sprintf("RSS (%s)", renderedBlogTitle), "href", a.getFullAddress(rd.Blog.Path+".rss")) + hb.WriteElementOpen("link", "rel", "alternate", "type", "application/atom+xml", "title", fmt.Sprintf("ATOM (%s)", renderedBlogTitle), "href", a.getFullAddress(rd.Blog.Path+".atom")) + hb.WriteElementOpen("link", "rel", "alternate", "type", "application/feed+json", "title", fmt.Sprintf("JSON Feed (%s)", renderedBlogTitle), "href", a.getFullAddress(rd.Blog.Path+".json")) // Webmentions - hb.writeElementOpen("link", "rel", "webmention", "href", a.getFullAddress("/webmention")) + hb.WriteElementOpen("link", "rel", "webmention", "href", a.getFullAddress("/webmention")) // Micropub - hb.writeElementOpen("link", "rel", "micropub", "href", "/micropub") + hb.WriteElementOpen("link", "rel", "micropub", "href", "/micropub") // IndieAuth - hb.writeElementOpen("link", "rel", "authorization_endpoint", "href", "/indieauth") - hb.writeElementOpen("link", "rel", "token_endpoint", "href", "/indieauth/token") - hb.writeElementOpen("link", "rel", "indieauth-metadata", "href", "/.well-known/oauth-authorization-server") + hb.WriteElementOpen("link", "rel", "authorization_endpoint", "href", "/indieauth") + hb.WriteElementOpen("link", "rel", "token_endpoint", "href", "/indieauth/token") + hb.WriteElementOpen("link", "rel", "indieauth-metadata", "href", "/.well-known/oauth-authorization-server") // Rel-Me user := a.cfg.User if user != nil { for _, i := range user.Identities { - hb.writeElementOpen("link", "rel", "me", "href", i) + hb.WriteElementOpen("link", "rel", "me", "href", i) } } // Opensearch if os := openSearchUrl(rd.Blog); os != "" { - hb.writeElementOpen("link", "rel", "search", "type", "application/opensearchdescription+xml", "href", os, "title", renderedBlogTitle) + hb.WriteElementOpen("link", "rel", "search", "type", "application/opensearchdescription+xml", "href", os, "title", renderedBlogTitle) } // Announcement if ann := rd.Blog.Announcement; ann != nil && ann.Text != "" { - hb.writeElementOpen("div", "id", "announcement", "data-nosnippet", "") + hb.WriteElementOpen("div", "id", "announcement", "data-nosnippet", "") _ = a.renderMarkdownToWriter(hb, ann.Text, false) - hb.writeElementClose("div") + hb.WriteElementClose("div") } // Header - hb.writeElementOpen("header") + hb.WriteElementOpen("header") // Blog title - hb.writeElementOpen("h1") - hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/"), "rel", "home", "title", renderedBlogTitle, "translate", "no") - hb.writeEscaped(renderedBlogTitle) - hb.writeElementClose("a") - hb.writeElementClose("h1") + hb.WriteElementOpen("h1") + hb.WriteElementOpen("a", "href", rd.Blog.getRelativePath("/"), "rel", "home", "title", renderedBlogTitle, "translate", "no") + hb.WriteEscaped(renderedBlogTitle) + hb.WriteElementClose("a") + hb.WriteElementClose("h1") // Blog description if rd.Blog.Description != "" { - hb.writeElementOpen("p") - hb.writeElementOpen("i") - hb.writeEscaped(rd.Blog.Description) - hb.writeElementClose("i") - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteElementOpen("i") + hb.WriteEscaped(rd.Blog.Description) + hb.WriteElementClose("i") + hb.WriteElementClose("p") } // Main menu if mm, ok := rd.Blog.Menus["main"]; ok { - hb.writeElementOpen("nav") + hb.WriteElementOpen("nav") for i, item := range mm.Items { if i > 0 { - hb.write(" • ") + hb.WriteUnescaped(" • ") } - hb.writeElementOpen("a", "href", item.Link) - hb.writeEscaped(a.renderMdTitle(item.Title)) - hb.writeElementClose("a") + hb.WriteElementOpen("a", "href", item.Link) + hb.WriteEscaped(a.renderMdTitle(item.Title)) + hb.WriteElementClose("a") } - hb.writeElementClose("nav") + hb.WriteElementClose("nav") } // Logged-in user menu if rd.LoggedIn() { - hb.writeElementOpen("nav") - hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/editor")) - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "editor")) - hb.writeElementClose("a") - hb.write(" • ") - hb.writeElementOpen("a", "href", "/notifications") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "notifications")) - hb.writeElementClose("a") + hb.WriteElementOpen("nav") + hb.WriteElementOpen("a", "href", rd.Blog.getRelativePath("/editor")) + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "editor")) + hb.WriteElementClose("a") + hb.WriteUnescaped(" • ") + hb.WriteElementOpen("a", "href", "/notifications") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "notifications")) + hb.WriteElementClose("a") if rd.WebmentionReceivingEnabled { - hb.write(" • ") - hb.writeElementOpen("a", "href", "/webmention") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "webmentions")) - hb.writeElementClose("a") + hb.WriteUnescaped(" • ") + hb.WriteElementOpen("a", "href", "/webmention") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "webmentions")) + hb.WriteElementClose("a") } if rd.CommentsEnabled { - hb.write(" • ") - hb.writeElementOpen("a", "href", "/comment") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "comments")) - hb.writeElementClose("a") + hb.WriteUnescaped(" • ") + hb.WriteElementOpen("a", "href", "/comment") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "comments")) + hb.WriteElementClose("a") } - hb.write(" • ") - hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/settings")) - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "settings")) - hb.writeElementClose("a") - hb.write(" • ") - hb.writeElementOpen("a", "href", "/logout") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "logout")) - hb.writeElementClose("a") - hb.writeElementClose("nav") + hb.WriteUnescaped(" • ") + hb.WriteElementOpen("a", "href", rd.Blog.getRelativePath("/settings")) + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "settings")) + hb.WriteElementClose("a") + hb.WriteUnescaped(" • ") + hb.WriteElementOpen("a", "href", "/logout") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "logout")) + hb.WriteElementClose("a") + hb.WriteElementClose("nav") } - hb.writeElementClose("header") + hb.WriteElementClose("header") // Main if main != nil { main(hb) } // Footer - hb.writeElementOpen("footer") + hb.WriteElementOpen("footer") // Footer menu if fm, ok := rd.Blog.Menus["footer"]; ok { - hb.writeElementOpen("nav") + hb.WriteElementOpen("nav") for i, item := range fm.Items { if i > 0 { - hb.write(" • ") + hb.WriteUnescaped(" • ") } - hb.writeElementOpen("a", "href", item.Link) - hb.writeEscaped(a.renderMdTitle(item.Title)) - hb.writeElementClose("a") + hb.WriteElementOpen("a", "href", item.Link) + hb.WriteEscaped(a.renderMdTitle(item.Title)) + hb.WriteElementClose("a") } - hb.writeElementClose("nav") + hb.WriteElementClose("nav") } // Copyright - hb.writeElementOpen("p", "translate", "no") - hb.write("© ") - hb.writeEscaped(time.Now().Format("2006")) - hb.write(" ") + hb.WriteElementOpen("p", "translate", "no") + hb.WriteUnescaped("© ") + hb.WriteEscaped(time.Now().Format("2006")) + hb.WriteUnescaped(" ") if user != nil && user.Name != "" { - hb.writeEscaped(user.Name) + hb.WriteEscaped(user.Name) } else { - hb.writeEscaped(renderedBlogTitle) + hb.WriteEscaped(renderedBlogTitle) } - hb.writeElementClose("p") + hb.WriteElementClose("p") // Tor a.renderTorNotice(hb, rd) - hb.writeElementClose("footer") + hb.WriteElementClose("footer") // Easter egg if rd.EasterEgg { - hb.writeElementOpen("script", "src", a.assetFileName("js/easteregg.js"), "defer", "") - hb.writeElementClose("script") + hb.WriteElementOpen("script", "src", a.assetFileName("js/easteregg.js"), "defer", "") + hb.WriteElementClose("script") } - hb.writeElementClose("html") + hb.WriteElementClose("html") } type errorRenderData struct { @@ -178,26 +202,26 @@ type errorRenderData struct { Message string } -func (a *goBlog) renderError(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderError(hb *htmlbuilder.HtmlBuilder, rd *renderData) { ed, ok := rd.Data.(*errorRenderData) if !ok { return } a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, ed.Title) }, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { if ed.Title != "" { - hb.writeElementOpen("h1") - hb.writeEscaped(ed.Title) - hb.writeElementClose("h1") + hb.WriteElementOpen("h1") + hb.WriteEscaped(ed.Title) + hb.WriteElementClose("h1") } if ed.Message != "" { - hb.writeElementOpen("p", "class", "monospace") - hb.writeEscaped(ed.Message) - hb.writeElementClose("p") + hb.WriteElementOpen("p", "class", "monospace") + hb.WriteEscaped(ed.Message) + hb.WriteElementClose("p") } }, ) @@ -208,64 +232,64 @@ type loginRenderData struct { totp bool } -func (a *goBlog) renderLogin(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderLogin(hb *htmlbuilder.HtmlBuilder, rd *renderData) { data, ok := rd.Data.(*loginRenderData) if !ok { return } a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, a.ts.GetTemplateStringVariant(rd.Blog.Lang, "login")) }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main") // Title - hb.writeElementOpen("h1") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "login")) - hb.writeElementClose("h1") + hb.WriteElementOpen("h1") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "login")) + hb.WriteElementClose("h1") // Form - hb.writeElementOpen("form", "class", "fw p", "method", "post") + hb.WriteElementOpen("form", "class", "fw p", "method", "post") // Hidden fields - hb.writeElementOpen("input", "type", "hidden", "name", "loginaction", "value", "login") - hb.writeElementOpen("input", "type", "hidden", "name", "loginmethod", "value", data.loginMethod) - hb.writeElementOpen("input", "type", "hidden", "name", "loginheaders", "value", data.loginHeaders) - hb.writeElementOpen("input", "type", "hidden", "name", "loginbody", "value", data.loginBody) + hb.WriteElementOpen("input", "type", "hidden", "name", "loginaction", "value", "login") + hb.WriteElementOpen("input", "type", "hidden", "name", "loginmethod", "value", data.loginMethod) + hb.WriteElementOpen("input", "type", "hidden", "name", "loginheaders", "value", data.loginHeaders) + hb.WriteElementOpen("input", "type", "hidden", "name", "loginbody", "value", data.loginBody) // Username - hb.writeElementOpen("input", "type", "text", "name", "username", "autocomplete", "username", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "username"), "required", "") + hb.WriteElementOpen("input", "type", "text", "name", "username", "autocomplete", "username", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "username"), "required", "") // Password - hb.writeElementOpen("input", "type", "password", "name", "password", "autocomplete", "current-password", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "password"), "required", "") + hb.WriteElementOpen("input", "type", "password", "name", "password", "autocomplete", "current-password", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "password"), "required", "") // TOTP if data.totp { - hb.writeElementOpen("input", "type", "text", "inputmode", "numeric", "pattern", "[0-9]*", "name", "token", "autocomplete", "one-time-code", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "totp"), "required", "") + hb.WriteElementOpen("input", "type", "text", "inputmode", "numeric", "pattern", "[0-9]*", "name", "token", "autocomplete", "one-time-code", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "totp"), "required", "") } // Submit - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "login")) - hb.writeElementClose("form") + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "login")) + hb.WriteElementClose("form") // Author (required for some IndieWeb apps) a.renderAuthor(hb) - hb.writeElementClose("main") + hb.WriteElementClose("main") }, ) } -func (a *goBlog) renderSearch(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderSearch(hb *htmlbuilder.HtmlBuilder, rd *renderData) { sc := rd.Blog.Search renderedSearchTitle := a.renderMdTitle(sc.Title) a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, renderedSearchTitle) }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main") titleOrDesc := false // Title if renderedSearchTitle != "" { titleOrDesc = true - hb.writeElementOpen("h1") - hb.writeEscaped(renderedSearchTitle) - hb.writeElementClose("h1") + hb.WriteElementOpen("h1") + hb.WriteEscaped(renderedSearchTitle) + hb.WriteElementClose("h1") } // Description if sc.Description != "" { @@ -273,66 +297,66 @@ func (a *goBlog) renderSearch(hb *htmlBuilder, rd *renderData) { _ = a.renderMarkdownToWriter(hb, sc.Description, false) } if titleOrDesc { - hb.writeElementOpen("hr") + hb.WriteElementOpen("hr") } // Form - hb.writeElementOpen("form", "class", "fw p", "method", "post") + hb.WriteElementOpen("form", "class", "fw p", "method", "post") // Search args := []any{"type", "text", "name", "q", "required", ""} if sc.Placeholder != "" { args = append(args, "placeholder", a.renderMdTitle(sc.Placeholder)) } - hb.writeElementOpen("input", args...) + hb.WriteElementOpen("input", args...) // Submit - hb.writeElementOpen("input", "type", "submit", "value", "🔍 "+a.ts.GetTemplateStringVariant(rd.Blog.Lang, "search")) - hb.writeElementClose("form") - hb.writeElementClose("main") + hb.WriteElementOpen("input", "type", "submit", "value", "🔍 "+a.ts.GetTemplateStringVariant(rd.Blog.Lang, "search")) + hb.WriteElementClose("form") + hb.WriteElementClose("main") }, ) } -func (a *goBlog) renderComment(h *htmlBuilder, rd *renderData) { +func (a *goBlog) renderComment(h *htmlbuilder.HtmlBuilder, rd *renderData) { c, ok := rd.Data.(*comment) if !ok { return } a.renderBase( h, rd, - func(hb *htmlBuilder) { - hb.writeElementOpen("title") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "acommentby")) - hb.write(" ") - hb.writeEscaped(c.Name) - hb.writeElementClose("title") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("title") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "acommentby")) + hb.WriteUnescaped(" ") + hb.WriteEscaped(c.Name) + hb.WriteElementClose("title") }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main", "class", "h-entry") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main", "class", "h-entry") // Target - hb.writeElementOpen("p") - hb.writeElementOpen("a", "class", "u-in-reply-to", "href", a.getFullAddress(c.Target)) - hb.writeEscaped(a.getFullAddress(c.Target)) - hb.writeElementClose("a") - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteElementOpen("a", "class", "u-in-reply-to", "href", a.getFullAddress(c.Target)) + hb.WriteEscaped(a.getFullAddress(c.Target)) + hb.WriteElementClose("a") + hb.WriteElementClose("p") // Author - hb.writeElementOpen("p", "class", "p-author h-card") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "acommentby")) - hb.write(" ") + hb.WriteElementOpen("p", "class", "p-author h-card") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "acommentby")) + hb.WriteUnescaped(" ") if c.Website != "" { - hb.writeElementOpen("a", "class", "p-name u-url", "target", "_blank", "rel", "nofollow noopener noreferrer ugc", "href", c.Website) - hb.writeEscaped(c.Name) - hb.writeElementClose("a") + hb.WriteElementOpen("a", "class", "p-name u-url", "target", "_blank", "rel", "nofollow noopener noreferrer ugc", "href", c.Website) + hb.WriteEscaped(c.Name) + hb.WriteElementClose("a") } else { - hb.writeElementOpen("span", "class", "p-name") - hb.writeEscaped(c.Name) - hb.writeElementClose("span") + hb.WriteElementOpen("span", "class", "p-name") + hb.WriteEscaped(c.Name) + hb.WriteElementClose("span") } - hb.writeEscaped(":") - hb.writeElementClose("p") + hb.WriteEscaped(":") + hb.WriteElementClose("p") // Content - hb.writeElementOpen("p", "class", "e-content") - hb.write(c.Comment) // Already escaped - hb.writeElementClose("p") - hb.writeElementClose("main") + hb.WriteElementOpen("p", "class", "e-content") + hb.WriteUnescaped(c.Comment) // Already escaped + hb.WriteElementClose("p") + hb.WriteElementClose("main") // Interactions if rd.CommentsEnabled { a.renderInteractions(hb, rd) @@ -349,7 +373,7 @@ type indexRenderData struct { summaryTemplate summaryTyp } -func (a *goBlog) renderIndex(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderIndex(hb *htmlbuilder.HtmlBuilder, rd *renderData) { id, ok := rd.Data.(*indexRenderData) if !ok { return @@ -357,7 +381,7 @@ func (a *goBlog) renderIndex(hb *htmlBuilder, rd *renderData) { renderedIndexTitle := a.renderMdTitle(id.title) a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { // Title a.renderTitleTag(hb, rd.Blog, renderedIndexTitle) // Feeds @@ -365,19 +389,19 @@ func (a *goBlog) renderIndex(hb *htmlBuilder, rd *renderData) { if renderedIndexTitle != "" { feedTitle = " (" + renderedIndexTitle + ")" } - hb.writeElementOpen("link", "rel", "alternate", "type", "application/rss+xml", "title", "RSS"+feedTitle, "href", a.getFullAddress(id.first+".rss")) - hb.writeElementOpen("link", "rel", "alternate", "type", "application/atom+xml", "title", "ATOM"+feedTitle, "href", a.getFullAddress(id.first+".atom")) - hb.writeElementOpen("link", "rel", "alternate", "type", "application/feed+json", "title", "JSON Feed"+feedTitle, "href", a.getFullAddress(id.first+".json")) + hb.WriteElementOpen("link", "rel", "alternate", "type", "application/rss+xml", "title", "RSS"+feedTitle, "href", a.getFullAddress(id.first+".rss")) + hb.WriteElementOpen("link", "rel", "alternate", "type", "application/atom+xml", "title", "ATOM"+feedTitle, "href", a.getFullAddress(id.first+".atom")) + hb.WriteElementOpen("link", "rel", "alternate", "type", "application/feed+json", "title", "JSON Feed"+feedTitle, "href", a.getFullAddress(id.first+".json")) }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main", "class", "h-feed") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main", "class", "h-feed") titleOrDesc := false // Title if renderedIndexTitle != "" { titleOrDesc = true - hb.writeElementOpen("h1", "class", "p-name") - hb.writeEscaped(renderedIndexTitle) - hb.writeElementClose("h1") + hb.WriteElementOpen("h1", "class", "p-name") + hb.WriteEscaped(renderedIndexTitle) + hb.WriteElementClose("h1") } // Description if id.description != "" { @@ -385,7 +409,7 @@ func (a *goBlog) renderIndex(hb *htmlBuilder, rd *renderData) { _ = a.renderMarkdownToWriter(hb, id.description, false) } if titleOrDesc { - hb.writeElementOpen("hr") + hb.WriteElementOpen("hr") } if id.posts != nil && len(id.posts) > 0 { // Posts @@ -394,15 +418,15 @@ func (a *goBlog) renderIndex(hb *htmlBuilder, rd *renderData) { } } else { // No posts - hb.writeElementOpen("p") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "noposts")) - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "noposts")) + hb.WriteElementClose("p") } // Navigation a.renderPagination(hb, rd.Blog, id.hasPrev, id.hasNext, id.prev, id.next) // Author a.renderAuthor(hb) - hb.writeElementClose("main") + hb.WriteElementClose("main") }, ) } @@ -411,7 +435,7 @@ type blogStatsRenderData struct { tableUrl string } -func (a *goBlog) renderBlogStats(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderBlogStats(hb *htmlbuilder.HtmlBuilder, rd *renderData) { bsd, ok := rd.Data.(*blogStatsRenderData) if !ok { return @@ -420,28 +444,28 @@ func (a *goBlog) renderBlogStats(hb *htmlBuilder, rd *renderData) { renderedBSTitle := a.renderMdTitle(bs.Title) a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, renderedBSTitle) }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main") // Title if renderedBSTitle != "" { - hb.writeElementOpen("h1") - hb.writeEscaped(renderedBSTitle) - hb.writeElementClose("h1") + hb.WriteElementOpen("h1") + hb.WriteEscaped(renderedBSTitle) + hb.WriteElementClose("h1") } // Description if bs.Description != "" { _ = a.renderMarkdownToWriter(hb, bs.Description, false) } // Table - hb.writeElementOpen("p", "id", "loading", "data-table", bsd.tableUrl) - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "loading")) - hb.writeElementClose("p") - hb.writeElementOpen("script", "src", a.assetFileName("js/blogstats.js"), "defer", "") - hb.writeElementClose("script") - hb.writeElementClose("main") + hb.WriteElementOpen("p", "id", "loading", "data-table", bsd.tableUrl) + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "loading")) + hb.WriteElementClose("p") + hb.WriteElementOpen("script", "src", a.assetFileName("js/blogstats.js"), "defer", "") + hb.WriteElementClose("script") + hb.WriteElementClose("main") // Interactions if rd.CommentsEnabled { a.renderInteractions(hb, rd) @@ -450,117 +474,117 @@ func (a *goBlog) renderBlogStats(hb *htmlBuilder, rd *renderData) { ) } -func (a *goBlog) renderBlogStatsTable(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderBlogStatsTable(hb *htmlbuilder.HtmlBuilder, rd *renderData) { bsd, ok := rd.Data.(*blogStatsData) if !ok { return } - hb.writeElementOpen("table") + hb.WriteElementOpen("table") // Table header - hb.writeElementOpen("thead") - hb.writeElementOpen("tr") + hb.WriteElementOpen("thead") + hb.WriteElementOpen("tr") // Year - hb.writeElementOpen("th", "class", "tal") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "year")) - hb.writeElementClose("th") + hb.WriteElementOpen("th", "class", "tal") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "year")) + hb.WriteElementClose("th") // Posts - hb.writeElementOpen("th", "class", "tar") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "posts")) - hb.writeElementClose("th") + hb.WriteElementOpen("th", "class", "tar") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "posts")) + hb.WriteElementClose("th") // Chars, Words, Words/Post for _, s := range []string{"chars", "words", "wordsperpost"} { - hb.writeElementOpen("th", "class", "tar") - hb.write("~") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, s)) - hb.writeElementClose("th") + hb.WriteElementOpen("th", "class", "tar") + hb.WriteUnescaped("~") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, s)) + hb.WriteElementClose("th") } - hb.writeElementClose("thead") + hb.WriteElementClose("thead") // Table body - hb.writeElementOpen("tbody") + hb.WriteElementOpen("tbody") // Iterate over years for _, y := range bsd.Years { // Stats for year - hb.writeElementOpen("tr", "class", "statsyear", "data-year", y.Name) - hb.writeElementOpen("td", "class", "tal") - hb.writeEscaped(y.Name) - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(y.Posts) - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(y.Chars) - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(y.Words) - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(y.WordsPerPost) - hb.writeElementClose("td") - hb.writeElementClose("tr") + hb.WriteElementOpen("tr", "class", "statsyear", "data-year", y.Name) + hb.WriteElementOpen("td", "class", "tal") + hb.WriteEscaped(y.Name) + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(y.Posts) + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(y.Chars) + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(y.Words) + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(y.WordsPerPost) + hb.WriteElementClose("td") + hb.WriteElementClose("tr") // Iterate over months for _, m := range bsd.Months[y.Name] { // Stats for month - hb.writeElementOpen("tr", "class", "statsmonth hide", "data-year", y.Name) - hb.writeElementOpen("td", "class", "tal") - hb.writeEscaped(y.Name) - hb.write("-") - hb.writeEscaped(m.Name) - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(m.Posts) - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(m.Chars) - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(m.Words) - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(m.WordsPerPost) - hb.writeElementClose("td") - hb.writeElementClose("tr") + hb.WriteElementOpen("tr", "class", "statsmonth hide", "data-year", y.Name) + hb.WriteElementOpen("td", "class", "tal") + hb.WriteEscaped(y.Name) + hb.WriteUnescaped("-") + hb.WriteEscaped(m.Name) + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(m.Posts) + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(m.Chars) + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(m.Words) + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(m.WordsPerPost) + hb.WriteElementClose("td") + hb.WriteElementClose("tr") } } // Posts without date - hb.writeElementOpen("tr") - hb.writeElementOpen("td", "class", "tal") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "withoutdate")) - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(bsd.NoDate.Posts) - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(bsd.NoDate.Chars) - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(bsd.NoDate.Words) - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(bsd.NoDate.WordsPerPost) - hb.writeElementClose("td") - hb.writeElementClose("tr") + hb.WriteElementOpen("tr") + hb.WriteElementOpen("td", "class", "tal") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "withoutdate")) + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(bsd.NoDate.Posts) + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(bsd.NoDate.Chars) + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(bsd.NoDate.Words) + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(bsd.NoDate.WordsPerPost) + hb.WriteElementClose("td") + hb.WriteElementClose("tr") // Total - hb.writeElementOpen("tr") - hb.writeElementOpen("td", "class", "tal") - hb.writeElementOpen("strong") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "total")) - hb.writeElementClose("strong") - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(bsd.Total.Posts) - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(bsd.Total.Chars) - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(bsd.Total.Words) - hb.writeElementClose("td") - hb.writeElementOpen("td", "class", "tar") - hb.writeEscaped(bsd.Total.WordsPerPost) - hb.writeElementClose("td") - hb.writeElementClose("tr") - hb.writeElementClose("tbody") - hb.writeElementClose("table") + hb.WriteElementOpen("tr") + hb.WriteElementOpen("td", "class", "tal") + hb.WriteElementOpen("strong") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "total")) + hb.WriteElementClose("strong") + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(bsd.Total.Posts) + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(bsd.Total.Chars) + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(bsd.Total.Words) + hb.WriteElementClose("td") + hb.WriteElementOpen("td", "class", "tar") + hb.WriteEscaped(bsd.Total.WordsPerPost) + hb.WriteElementClose("td") + hb.WriteElementClose("tr") + hb.WriteElementClose("tbody") + hb.WriteElementClose("table") } type geoMapRenderData struct { @@ -572,24 +596,24 @@ type geoMapRenderData struct { maxZoom int } -func (a *goBlog) renderGeoMap(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderGeoMap(hb *htmlbuilder.HtmlBuilder, rd *renderData) { gmd, ok := rd.Data.(*geoMapRenderData) if !ok { return } a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, "") }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main") if gmd.noLocations { - hb.writeElementOpen("p") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "nolocations")) - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "nolocations")) + hb.WriteElementClose("p") } else { - hb.writeElementOpen( + hb.WriteElementOpen( "div", "id", "map", "class", "p", "data-locations", gmd.locations, "data-tracks", gmd.tracks, @@ -597,11 +621,11 @@ func (a *goBlog) renderGeoMap(hb *htmlBuilder, rd *renderData) { "data-maxzoom", gmd.maxZoom, "data-attribution", gmd.attribution, ) - hb.writeElementClose("div") - hb.writeElementOpen("script", "src", a.assetFileName("js/geomap.js")) - hb.writeElementClose("script") + hb.WriteElementClose("div") + hb.WriteElementOpen("script", "src", a.assetFileName("js/geomap.js")) + hb.WriteElementClose("script") } - hb.writeElementClose("main") + hb.WriteElementClose("main") if rd.CommentsEnabled { a.renderInteractions(hb, rd) } @@ -616,7 +640,7 @@ type blogrollRenderData struct { download string } -func (a *goBlog) renderBlogroll(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderBlogroll(hb *htmlbuilder.HtmlBuilder, rd *renderData) { bd, ok := rd.Data.(*blogrollRenderData) if !ok { return @@ -624,58 +648,58 @@ func (a *goBlog) renderBlogroll(hb *htmlBuilder, rd *renderData) { renderedTitle := a.renderMdTitle(bd.title) a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, renderedTitle) }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main") // Title if renderedTitle != "" { - hb.writeElementOpen("h1") - hb.writeEscaped(renderedTitle) - hb.writeElementClose("h1") + hb.WriteElementOpen("h1") + hb.WriteEscaped(renderedTitle) + hb.WriteElementClose("h1") } // Description if bd.description != "" { - hb.writeElementOpen("p") + hb.WriteElementOpen("p") _ = a.renderMarkdownToWriter(hb, bd.description, false) - hb.writeElementClose("p") + hb.WriteElementClose("p") } // Download button - hb.writeElementOpen("p") - hb.writeElementOpen("a", "href", rd.Blog.getRelativePath(bd.download), "class", "button", "download", "") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "download")) - hb.writeElementClose("a") - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteElementOpen("a", "href", rd.Blog.getRelativePath(bd.download), "class", "button", "download", "") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "download")) + hb.WriteElementClose("a") + hb.WriteElementClose("p") // Outlines for _, outline := range bd.outlines { title := outline.Title if title == "" { title = outline.Text } - hb.writeElementOpen("h2", "id", urlize(title)) - hb.writeEscaped(fmt.Sprintf("%s (%d)", title, len(outline.Outlines))) - hb.writeElementClose("h2") - hb.writeElementOpen("ul") + hb.WriteElementOpen("h2", "id", urlize(title)) + hb.WriteEscaped(fmt.Sprintf("%s (%d)", title, len(outline.Outlines))) + hb.WriteElementClose("h2") + hb.WriteElementOpen("ul") for _, subOutline := range outline.Outlines { subTitle := subOutline.Title if subTitle == "" { subTitle = subOutline.Text } - hb.writeElementOpen("li") - hb.writeElementOpen("a", "href", subOutline.HTMLURL, "target", "_blank") - hb.writeEscaped(subTitle) - hb.writeElementClose("a") - hb.write(" (") - hb.writeElementOpen("a", "href", subOutline.XMLURL, "target", "_blank") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "feed")) - hb.writeElementClose("a") - hb.write(")") - hb.writeElementClose("li") + hb.WriteElementOpen("li") + hb.WriteElementOpen("a", "href", subOutline.HTMLURL, "target", "_blank") + hb.WriteEscaped(subTitle) + hb.WriteElementClose("a") + hb.WriteUnescaped(" (") + hb.WriteElementOpen("a", "href", subOutline.XMLURL, "target", "_blank") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "feed")) + hb.WriteElementClose("a") + hb.WriteUnescaped(")") + hb.WriteElementClose("li") } - hb.writeElementClose("ul") + hb.WriteElementClose("ul") } - hb.writeElementClose("main") + hb.WriteElementClose("main") // Interactions if rd.CommentsEnabled { a.renderInteractions(hb, rd) @@ -691,7 +715,7 @@ type contactRenderData struct { sent bool } -func (a *goBlog) renderContact(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderContact(hb *htmlbuilder.HtmlBuilder, rd *renderData) { cd, ok := rd.Data.(*contactRenderData) if !ok { return @@ -699,49 +723,49 @@ func (a *goBlog) renderContact(hb *htmlBuilder, rd *renderData) { renderedTitle := a.renderMdTitle(cd.title) a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, renderedTitle) }, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { if cd.sent { - hb.writeElementOpen("main") - hb.writeElementOpen("p") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "messagesent")) - hb.writeElementClose("p") - hb.writeElementClose("main") + hb.WriteElementOpen("main") + hb.WriteElementOpen("p") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "messagesent")) + hb.WriteElementClose("p") + hb.WriteElementClose("main") return } - hb.writeElementOpen("main") + hb.WriteElementOpen("main") // Title if renderedTitle != "" { - hb.writeElementOpen("h1") - hb.writeEscaped(renderedTitle) - hb.writeElementClose("h1") + hb.WriteElementOpen("h1") + hb.WriteEscaped(renderedTitle) + hb.WriteElementClose("h1") } // Description if cd.description != "" { _ = a.renderMarkdownToWriter(hb, cd.description, false) } // Form - hb.writeElementOpen("form", "class", "fw p", "method", "post") + hb.WriteElementOpen("form", "class", "fw p", "method", "post") // Name (optional) - hb.writeElementOpen("input", "type", "text", "name", "name", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "nameopt")) + hb.WriteElementOpen("input", "type", "text", "name", "name", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "nameopt")) // Website (optional) - hb.writeElementOpen("input", "type", "url", "name", "website", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "websiteopt")) + hb.WriteElementOpen("input", "type", "url", "name", "website", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "websiteopt")) // Email (optional) - hb.writeElementOpen("input", "type", "email", "name", "email", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "emailopt")) + hb.WriteElementOpen("input", "type", "email", "name", "email", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "emailopt")) // Message (required) - hb.writeElementOpen("textarea", "name", "message", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "message"), "required", "") - hb.writeElementClose("textarea") + hb.WriteElementOpen("textarea", "name", "message", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "message"), "required", "") + hb.WriteElementClose("textarea") // Send if cd.privacy != "" { _ = a.renderMarkdownToWriter(hb, cd.privacy, false) - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "contactagreesend")) + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "contactagreesend")) } else { - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "contactsend")) + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "contactsend")) } - hb.writeElementClose("form") - hb.writeElementClose("main") + hb.WriteElementClose("form") + hb.WriteElementClose("main") }, ) } @@ -753,35 +777,35 @@ type captchaRenderData struct { captchaId string } -func (a *goBlog) renderCaptcha(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderCaptcha(hb *htmlbuilder.HtmlBuilder, rd *renderData) { crd, ok := rd.Data.(*captchaRenderData) if !ok { return } a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, "") }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main") // Captcha image - hb.writeElementOpen("p") - hb.writeElementOpen("img", "src", "/captcha/"+crd.captchaId+".png", "class", "captchaimg") - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteElementOpen("img", "src", "/captcha/"+crd.captchaId+".png", "class", "captchaimg") + hb.WriteElementClose("p") // Form - hb.writeElementOpen("form", "class", "fw p", "method", "post") + hb.WriteElementOpen("form", "class", "fw p", "method", "post") // Hidden fields - hb.writeElementOpen("input", "type", "hidden", "name", "captchaaction", "value", "captcha") - hb.writeElementOpen("input", "type", "hidden", "name", "captchamethod", "value", crd.captchaMethod) - hb.writeElementOpen("input", "type", "hidden", "name", "captchaheaders", "value", crd.captchaHeaders) - hb.writeElementOpen("input", "type", "hidden", "name", "captchabody", "value", crd.captchaBody) + hb.WriteElementOpen("input", "type", "hidden", "name", "captchaaction", "value", "captcha") + hb.WriteElementOpen("input", "type", "hidden", "name", "captchamethod", "value", crd.captchaMethod) + hb.WriteElementOpen("input", "type", "hidden", "name", "captchaheaders", "value", crd.captchaHeaders) + hb.WriteElementOpen("input", "type", "hidden", "name", "captchabody", "value", crd.captchaBody) // Text - hb.writeElementOpen("input", "type", "text", "name", "digits", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "captchainstructions"), "required", "") + hb.WriteElementOpen("input", "type", "text", "name", "digits", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "captchainstructions"), "required", "") // Submit - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "submit")) - hb.writeElementClose("form") - hb.writeElementClose("main") + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "submit")) + hb.WriteElementClose("form") + hb.WriteElementClose("main") }, ) } @@ -791,7 +815,7 @@ type taxonomyRenderData struct { valueGroups []stringGroup } -func (a *goBlog) renderTaxonomy(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderTaxonomy(hb *htmlbuilder.HtmlBuilder, rd *renderData) { trd, ok := rd.Data.(*taxonomyRenderData) if !ok { return @@ -799,16 +823,16 @@ func (a *goBlog) renderTaxonomy(hb *htmlBuilder, rd *renderData) { renderedTitle := a.renderMdTitle(trd.taxonomy.Title) a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, renderedTitle) }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main") // Title if renderedTitle != "" { - hb.writeElementOpen("h1") - hb.writeEscaped(renderedTitle) - hb.writeElementClose("h1") + hb.WriteElementOpen("h1") + hb.WriteEscaped(renderedTitle) + hb.WriteElementClose("h1") } // Description if trd.taxonomy.Description != "" { @@ -817,140 +841,143 @@ func (a *goBlog) renderTaxonomy(hb *htmlBuilder, rd *renderData) { // List for _, valGroup := range trd.valueGroups { // Title - hb.writeElementOpen("h2") - hb.writeEscaped(valGroup.Identifier) - hb.writeElementClose("h2") + hb.WriteElementOpen("h2") + hb.WriteEscaped(valGroup.Identifier) + hb.WriteElementClose("h2") // List - hb.writeElementOpen("p") + hb.WriteElementOpen("p") for i, val := range valGroup.Strings { if i > 0 { - hb.write(" • ") + hb.WriteUnescaped(" • ") } - hb.writeElementOpen("a", "href", rd.Blog.getRelativePath(fmt.Sprintf("/%s/%s", trd.taxonomy.Name, urlize(val)))) - hb.writeEscaped(val) - hb.writeElementClose("a") + hb.WriteElementOpen("a", "href", rd.Blog.getRelativePath(fmt.Sprintf("/%s/%s", trd.taxonomy.Name, urlize(val)))) + hb.WriteEscaped(val) + hb.WriteElementClose("a") } - hb.writeElementClose("p") + hb.WriteElementClose("p") } }, ) } -func (a *goBlog) renderPost(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderPost(hb *htmlbuilder.HtmlBuilder, rd *renderData) { p, ok := rd.Data.(*post) if !ok { return } a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, p.RenderedTitle) - hb.writeElementOpen("link", "rel", "stylesheet", "href", a.assetFileName("css/chroma.css")) + hb.WriteElementOpen("link", "rel", "stylesheet", "href", a.assetFileName("css/chroma.css")) a.renderPostHeadMeta(hb, p, rd.Canonical) if su := a.shortPostURL(p); su != "" { - hb.writeElementOpen("link", "rel", "shortlink", "href", su) + hb.WriteElementOpen("link", "rel", "shortlink", "href", su) } }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main", "class", "h-entry") - hb.writeElementOpen("article") - // URL (hidden just for microformats) - hb.writeElementOpen("data", "value", a.getFullAddress(p.Path), "class", "u-url hide") - hb.writeElementClose("data") - // Title - a.renderPostTitle(hb, p) - // Post meta - a.renderPostMeta(hb, p, rd.Blog, "post") - // Post actions - hb.writeElementOpen("div", "class", "actions") - // Share button - hb.writeElementOpen("a", "class", "button", "href", fmt.Sprintf("https://www.addtoany.com/share#url=%s%s", a.shortPostURL(p), lo.If(p.RenderedTitle != "", "&title="+p.RenderedTitle).Else("")), "target", "_blank", "rel", "nofollow noopener noreferrer") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "share")) - hb.writeElementClose("a") - // Translate button - hb.writeElementOpen( - "a", "id", "translateBtn", - "class", "button", - "href", fmt.Sprintf("https://translate.google.com/translate?u=%s", a.getFullAddress(p.Path)), - "target", "_blank", "rel", "nofollow noopener noreferrer", - "title", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "translate"), - "translate", "no", - ) - hb.writeEscaped("A ⇄ 文") - hb.writeElementClose("a") - hb.writeElementOpen("script", "defer", "", "src", a.assetFileName("js/translate.js")) - hb.writeElementClose("script") - // Speak button - hb.writeElementOpen("button", "id", "speakBtn", "class", "hide", "data-speak", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "speak"), "data-stopspeak", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "stopspeak")) - hb.writeElementClose("button") - hb.writeElementOpen("script", "defer", "", "src", lo.If(p.TTS() != "", a.assetFileName("js/tts.js")).Else(a.assetFileName("js/speak.js"))) - hb.writeElementClose("script") - // Close post actions - hb.writeElementClose("div") - // TTS - if tts := p.TTS(); tts != "" { - hb.writeElementOpen("div", "class", "p hide", "id", "tts") - hb.writeElementOpen("audio", "controls", "", "preload", "none", "id", "tts-audio") - hb.writeElementOpen("source", "src", tts) - hb.writeElementClose("source") - hb.writeElementClose("audio") - hb.writeElementClose("div") - } - // Old content warning - a.renderOldContentWarning(hb, p, rd.Blog) - // Content - if p.Content != "" { + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main", "class", "h-entry") + a.renderWithPlugins(hb, plugintypes.PostMainElementRenderType, p.pluginRenderData(), func(hb *htmlbuilder.HtmlBuilder) { + // URL (hidden just for microformats) + hb.WriteElementOpen("data", "value", a.getFullAddress(p.Path), "class", "u-url hide") + hb.WriteElementClose("data") + // Start article + hb.WriteElementOpen("article") + // Title + a.renderPostTitle(hb, p) + // Post meta + a.renderPostMeta(hb, p, rd.Blog, "post") + // Post actions + hb.WriteElementOpen("div", "class", "actions") + // Share button + hb.WriteElementOpen("a", "class", "button", "href", fmt.Sprintf("https://www.addtoany.com/share#url=%s%s", a.shortPostURL(p), lo.If(p.RenderedTitle != "", "&title="+p.RenderedTitle).Else("")), "target", "_blank", "rel", "nofollow noopener noreferrer") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "share")) + hb.WriteElementClose("a") + // Translate button + hb.WriteElementOpen( + "a", "id", "translateBtn", + "class", "button", + "href", fmt.Sprintf("https://translate.google.com/translate?u=%s", a.getFullAddress(p.Path)), + "target", "_blank", "rel", "nofollow noopener noreferrer", + "title", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "translate"), + "translate", "no", + ) + hb.WriteEscaped("A ⇄ 文") + hb.WriteElementClose("a") + hb.WriteElementOpen("script", "defer", "", "src", a.assetFileName("js/translate.js")) + hb.WriteElementClose("script") + // Speak button + hb.WriteElementOpen("button", "id", "speakBtn", "class", "hide", "data-speak", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "speak"), "data-stopspeak", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "stopspeak")) + hb.WriteElementClose("button") + hb.WriteElementOpen("script", "defer", "", "src", lo.If(p.TTS() != "", a.assetFileName("js/tts.js")).Else(a.assetFileName("js/speak.js"))) + hb.WriteElementClose("script") + // Close post actions + hb.WriteElementClose("div") + // TTS + if tts := p.TTS(); tts != "" { + hb.WriteElementOpen("div", "class", "p hide", "id", "tts") + hb.WriteElementOpen("audio", "controls", "", "preload", "none", "id", "tts-audio") + hb.WriteElementOpen("source", "src", tts) + hb.WriteElementClose("source") + hb.WriteElementClose("audio") + hb.WriteElementClose("div") + } + // Old content warning + a.renderOldContentWarning(hb, p, rd.Blog) // Content - hb.writeElementOpen("div", "class", "e-content") - a.postHtmlToWriter(hb, p, false) - hb.writeElementClose("div") - } - // External Videp - a.renderPostVideo(hb, p) - // GPS Track - a.renderPostGPX(hb, p, rd.Blog) - // Taxonomies - a.renderPostTax(hb, p, rd.Blog) - hb.writeElementClose("article") - // Author - a.renderAuthor(hb) - hb.writeElementClose("main") + if p.Content != "" { + // Content + hb.WriteElementOpen("div", "class", "e-content") + a.postHtmlToWriter(hb, p, false) + hb.WriteElementClose("div") + } + // External Videp + a.renderPostVideo(hb, p) + // GPS Track + a.renderPostGPX(hb, p, rd.Blog) + // Taxonomies + a.renderPostTax(hb, p, rd.Blog) + hb.WriteElementClose("article") + // Author + a.renderAuthor(hb) + }) + hb.WriteElementClose("main") // Reactions a.renderPostReactions(hb, p) // Post edit actions if rd.LoggedIn() { - hb.writeElementOpen("div", "class", "actions") + hb.WriteElementOpen("div", "class", "actions") // Update - hb.writeElementOpen("form", "method", "post", "action", rd.Blog.getRelativePath("/editor")+"#update") - hb.writeElementOpen("input", "type", "hidden", "name", "editoraction", "value", "loadupdate") - hb.writeElementOpen("input", "type", "hidden", "name", "path", "value", p.Path) - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update")) - hb.writeElementClose("form") + hb.WriteElementOpen("form", "method", "post", "action", rd.Blog.getRelativePath("/editor")+"#update") + hb.WriteElementOpen("input", "type", "hidden", "name", "editoraction", "value", "loadupdate") + hb.WriteElementOpen("input", "type", "hidden", "name", "path", "value", p.Path) + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update")) + hb.WriteElementClose("form") // Delete - hb.writeElementOpen("form", "method", "post", "action", rd.Blog.getRelativePath("/editor")) - hb.writeElementOpen("input", "type", "hidden", "name", "action", "value", "delete") - hb.writeElementOpen("input", "type", "hidden", "name", "url", "value", rd.Canonical) - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete"), "class", "confirm", "data-confirmmessage", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "confirmdelete")) - hb.writeElementClose("form") + hb.WriteElementOpen("form", "method", "post", "action", rd.Blog.getRelativePath("/editor")) + hb.WriteElementOpen("input", "type", "hidden", "name", "action", "value", "delete") + hb.WriteElementOpen("input", "type", "hidden", "name", "url", "value", rd.Canonical) + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete"), "class", "confirm", "data-confirmmessage", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "confirmdelete")) + hb.WriteElementClose("form") // Undelete if p.Deleted() { - hb.writeElementOpen("form", "method", "post", "action", rd.Blog.getRelativePath("/editor")) - hb.writeElementOpen("input", "type", "hidden", "name", "action", "value", "undelete") - hb.writeElementOpen("input", "type", "hidden", "name", "url", "value", rd.Canonical) - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "undelete")) - hb.writeElementClose("form") + hb.WriteElementOpen("form", "method", "post", "action", rd.Blog.getRelativePath("/editor")) + hb.WriteElementOpen("input", "type", "hidden", "name", "action", "value", "undelete") + hb.WriteElementOpen("input", "type", "hidden", "name", "url", "value", rd.Canonical) + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "undelete")) + hb.WriteElementClose("form") } // TTS if a.ttsEnabled() { - hb.writeElementOpen("form", "method", "post", "action", rd.Blog.getRelativePath("/editor")) - hb.writeElementOpen("input", "type", "hidden", "name", "editoraction", "value", "tts") - hb.writeElementOpen("input", "type", "hidden", "name", "url", "value", rd.Canonical) - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "gentts")) - hb.writeElementClose("form") + hb.WriteElementOpen("form", "method", "post", "action", rd.Blog.getRelativePath("/editor")) + hb.WriteElementOpen("input", "type", "hidden", "name", "editoraction", "value", "tts") + hb.WriteElementOpen("input", "type", "hidden", "name", "url", "value", rd.Canonical) + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "gentts")) + hb.WriteElementClose("form") } - hb.writeElementOpen("script", "defer", "", "src", a.assetFileName("js/formconfirm.js")) - hb.writeElementClose("script") - hb.writeElementClose("div") + hb.WriteElementOpen("script", "defer", "", "src", a.assetFileName("js/formconfirm.js")) + hb.WriteElementClose("script") + hb.WriteElementClose("div") } // Comments if rd.CommentsEnabled { @@ -960,108 +987,108 @@ func (a *goBlog) renderPost(hb *htmlBuilder, rd *renderData) { ) } -func (a *goBlog) renderStaticHome(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderStaticHome(hb *htmlbuilder.HtmlBuilder, rd *renderData) { p, ok := rd.Data.(*post) if !ok { return } a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, "") a.renderPostHeadMeta(hb, p, rd.Canonical) }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main", "class", "h-entry") - hb.writeElementOpen("article") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main", "class", "h-entry") + hb.WriteElementOpen("article") // URL (hidden just for microformats) - hb.writeElementOpen("data", "value", a.getFullAddress(p.Path), "class", "u-url hide") - hb.writeElementClose("data") + hb.WriteElementOpen("data", "value", a.getFullAddress(p.Path), "class", "u-url hide") + hb.WriteElementClose("data") // Content if p.Content != "" { // Content - hb.writeElementOpen("div", "class", "e-content") + hb.WriteElementOpen("div", "class", "e-content") a.postHtmlToWriter(hb, p, false) - hb.writeElementClose("div") + hb.WriteElementClose("div") } // Author a.renderAuthor(hb) - hb.writeElementClose("article") - hb.writeElementClose("main") + hb.WriteElementClose("article") + hb.WriteElementClose("main") // Update if rd.LoggedIn() { - hb.writeElementOpen("div", "class", "actions") - hb.writeElementOpen("form", "method", "post", "action", rd.Blog.getRelativePath("/editor")+"#update") - hb.writeElementOpen("input", "type", "hidden", "name", "editoraction", "value", "loadupdate") - hb.writeElementOpen("input", "type", "hidden", "name", "path", "value", p.Path) - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update")) - hb.writeElementClose("form") - hb.writeElementClose("div") + hb.WriteElementOpen("div", "class", "actions") + hb.WriteElementOpen("form", "method", "post", "action", rd.Blog.getRelativePath("/editor")+"#update") + hb.WriteElementOpen("input", "type", "hidden", "name", "editoraction", "value", "loadupdate") + hb.WriteElementOpen("input", "type", "hidden", "name", "path", "value", p.Path) + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update")) + hb.WriteElementClose("form") + hb.WriteElementClose("div") } }, ) } -func (a *goBlog) renderIndieAuth(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderIndieAuth(hb *htmlbuilder.HtmlBuilder, rd *renderData) { indieAuthRequest, ok := rd.Data.(*indieauth.AuthenticationRequest) if !ok { return } a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, a.ts.GetTemplateStringVariant(rd.Blog.Lang, "indieauth")) }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main") // Title - hb.writeElementOpen("h1") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "indieauth")) - hb.writeElementClose("h1") - hb.writeElementClose("main") + hb.WriteElementOpen("h1") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "indieauth")) + hb.WriteElementClose("h1") + hb.WriteElementClose("main") // Form - hb.writeElementOpen("form", "method", "post", "action", "/indieauth/accept", "class", "p") + hb.WriteElementOpen("form", "method", "post", "action", "/indieauth/accept", "class", "p") // Scopes if scopes := indieAuthRequest.Scopes; len(scopes) > 0 { - hb.writeElementOpen("h3") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "scopes")) - hb.writeElementClose("h3") - hb.writeElementOpen("ul") + hb.WriteElementOpen("h3") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "scopes")) + hb.WriteElementClose("h3") + hb.WriteElementOpen("ul") for _, scope := range scopes { - hb.writeElementOpen("li") - hb.writeElementOpen("input", "type", "checkbox", "name", "scopes", "value", scope, "id", "scope-"+scope, "checked", "") - hb.writeElementOpen("label", "for", "scope-"+scope) - hb.writeEscaped(scope) - hb.writeElementClose("label") - hb.writeElementClose("li") + hb.WriteElementOpen("li") + hb.WriteElementOpen("input", "type", "checkbox", "name", "scopes", "value", scope, "id", "scope-"+scope, "checked", "") + hb.WriteElementOpen("label", "for", "scope-"+scope) + hb.WriteEscaped(scope) + hb.WriteElementClose("label") + hb.WriteElementClose("li") } - hb.writeElementClose("ul") + hb.WriteElementClose("ul") } // Client ID - hb.writeElementOpen("p") - hb.writeElementOpen("strong") - hb.writeEscaped("client_id:") - hb.writeElementClose("strong") - hb.write(" ") - hb.writeEscaped(indieAuthRequest.ClientID) - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteElementOpen("strong") + hb.WriteEscaped("client_id:") + hb.WriteElementClose("strong") + hb.WriteUnescaped(" ") + hb.WriteEscaped(indieAuthRequest.ClientID) + hb.WriteElementClose("p") // Redirect URI - hb.writeElementOpen("p") - hb.writeElementOpen("strong") - hb.writeEscaped("redirect_uri:") - hb.writeElementClose("strong") - hb.write(" ") - hb.writeEscaped(indieAuthRequest.RedirectURI) - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteElementOpen("strong") + hb.WriteEscaped("redirect_uri:") + hb.WriteElementClose("strong") + hb.WriteUnescaped(" ") + hb.WriteEscaped(indieAuthRequest.RedirectURI) + hb.WriteElementClose("p") // Hidden form fields - hb.writeElementOpen("input", "type", "hidden", "name", "client_id", "value", indieAuthRequest.ClientID) - hb.writeElementOpen("input", "type", "hidden", "name", "redirect_uri", "value", indieAuthRequest.RedirectURI) - hb.writeElementOpen("input", "type", "hidden", "name", "state", "value", indieAuthRequest.State) - hb.writeElementOpen("input", "type", "hidden", "name", "code_challenge", "value", indieAuthRequest.CodeChallenge) - hb.writeElementOpen("input", "type", "hidden", "name", "code_challenge_method", "value", indieAuthRequest.CodeChallengeMethod) + hb.WriteElementOpen("input", "type", "hidden", "name", "client_id", "value", indieAuthRequest.ClientID) + hb.WriteElementOpen("input", "type", "hidden", "name", "redirect_uri", "value", indieAuthRequest.RedirectURI) + hb.WriteElementOpen("input", "type", "hidden", "name", "state", "value", indieAuthRequest.State) + hb.WriteElementOpen("input", "type", "hidden", "name", "code_challenge", "value", indieAuthRequest.CodeChallenge) + hb.WriteElementOpen("input", "type", "hidden", "name", "code_challenge_method", "value", indieAuthRequest.CodeChallengeMethod) // Submit button - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "authenticate")) - hb.writeElementClose("form") + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "authenticate")) + hb.WriteElementClose("form") }, ) } @@ -1071,55 +1098,55 @@ type editorFilesRenderData struct { uses []int } -func (a *goBlog) renderEditorFiles(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderEditorFiles(hb *htmlbuilder.HtmlBuilder, rd *renderData) { ef, ok := rd.Data.(*editorFilesRenderData) if !ok { return } a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, a.ts.GetTemplateStringVariant(rd.Blog.Lang, "mediafiles")) }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main") // Title - hb.writeElementOpen("h1") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "mediafiles")) - hb.writeElementClose("h1") + hb.WriteElementOpen("h1") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "mediafiles")) + hb.WriteElementClose("h1") // Files if len(ef.files) > 0 { // Form - hb.writeElementOpen("form", "method", "post", "class", "fw p") + hb.WriteElementOpen("form", "method", "post", "class", "fw p") // Select with number of uses - hb.writeElementOpen("select", "name", "filename") + hb.WriteElementOpen("select", "name", "filename") usesString := a.ts.GetTemplateStringVariant(rd.Blog.Lang, "fileuses") for i, f := range ef.files { - hb.writeElementOpen("option", "value", f.Name) - hb.writeEscaped(fmt.Sprintf("%s (%s), %s, ~%d %s", f.Name, f.Time.Local().Format(isoDateFormat), mBytesString(f.Size), ef.uses[i], usesString)) - hb.writeElementClose("option") + hb.WriteElementOpen("option", "value", f.Name) + hb.WriteEscaped(fmt.Sprintf("%s (%s), %s, ~%d %s", f.Name, f.Time.Local().Format(isoDateFormat), mBytesString(f.Size), ef.uses[i], usesString)) + hb.WriteElementClose("option") } - hb.writeElementClose("select") + hb.WriteElementClose("select") // View button - hb.writeElementOpen( + hb.WriteElementOpen( "input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "view"), "formaction", rd.Blog.getRelativePath("/editor/files/view"), ) // Delete button - hb.writeElementOpen( + hb.WriteElementOpen( "input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete"), "formaction", rd.Blog.getRelativePath("/editor/files/delete"), "class", "confirm", "data-confirmmessage", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "confirmdelete"), ) - hb.writeElementOpen("script", "src", a.assetFileName("js/formconfirm.js"), "defer", "") - hb.writeElementClose("script") - hb.writeElementClose("form") + hb.WriteElementOpen("script", "src", a.assetFileName("js/formconfirm.js"), "defer", "") + hb.WriteElementClose("script") + hb.WriteElementClose("form") } else { - hb.writeElementOpen("p") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "nofiles")) - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "nofiles")) + hb.WriteElementClose("p") } - hb.writeElementClose("main") + hb.WriteElementClose("main") }, ) } @@ -1130,50 +1157,50 @@ type notificationsRenderData struct { prev, next string } -func (a *goBlog) renderNotificationsAdmin(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderNotificationsAdmin(hb *htmlbuilder.HtmlBuilder, rd *renderData) { nrd, ok := rd.Data.(*notificationsRenderData) if !ok { return } a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, a.ts.GetTemplateStringVariant(rd.Blog.Lang, "notifications")) }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main") // Title - hb.writeElementOpen("h1") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "notifications")) - hb.writeElementClose("h1") + hb.WriteElementOpen("h1") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "notifications")) + hb.WriteElementClose("h1") // Delete all form - hb.writeElementOpen("form", "class", "actions", "method", "post", "action", "/notifications/delete") - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "deleteall")) - hb.writeElementClose("form") + hb.WriteElementOpen("form", "class", "actions", "method", "post", "action", "/notifications/delete") + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "deleteall")) + hb.WriteElementClose("form") // Notifications tdLocale := matchTimeDiffLocale(rd.Blog.Lang) for _, n := range nrd.notifications { - hb.writeElementOpen("div", "class", "p") + hb.WriteElementOpen("div", "class", "p") // Date - hb.writeElementOpen("p") - hb.writeElementOpen("i") - hb.writeEscaped(timediff.TimeDiff(time.Unix(n.Time, 0), timediff.WithLocale(tdLocale))) - hb.writeElementClose("i") - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteElementOpen("i") + hb.WriteEscaped(timediff.TimeDiff(time.Unix(n.Time, 0), timediff.WithLocale(tdLocale))) + hb.WriteElementClose("i") + hb.WriteElementClose("p") // Message - hb.writeElementOpen("pre") - hb.writeEscaped(n.Text) - hb.writeElementClose("pre") + hb.WriteElementOpen("pre") + hb.WriteEscaped(n.Text) + hb.WriteElementClose("pre") // Delete form - hb.writeElementOpen("form", "class", "actions", "method", "post", "action", "/notifications/delete") - hb.writeElementOpen("input", "type", "hidden", "name", "notificationid", "value", n.ID) - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete")) - hb.writeElementClose("form") - hb.writeElementClose("div") + hb.WriteElementOpen("form", "class", "actions", "method", "post", "action", "/notifications/delete") + hb.WriteElementOpen("input", "type", "hidden", "name", "notificationid", "value", n.ID) + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete")) + hb.WriteElementClose("form") + hb.WriteElementClose("div") } // Pagination a.renderPagination(hb, rd.Blog, nrd.hasPrev, nrd.hasNext, nrd.prev, nrd.next) - hb.writeElementClose("main") + hb.WriteElementClose("main") }, ) } @@ -1184,58 +1211,58 @@ type commentsRenderData struct { prev, next string } -func (a *goBlog) renderCommentsAdmin(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderCommentsAdmin(hb *htmlbuilder.HtmlBuilder, rd *renderData) { crd, ok := rd.Data.(*commentsRenderData) if !ok { return } a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, a.ts.GetTemplateStringVariant(rd.Blog.Lang, "comments")) }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main") // Title - hb.writeElementOpen("h1") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "comments")) - hb.writeElementClose("h1") + hb.WriteElementOpen("h1") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "comments")) + hb.WriteElementClose("h1") // Notifications for _, c := range crd.comments { - hb.writeElementOpen("div", "class", "p") + hb.WriteElementOpen("div", "class", "p") // ID, Target, Name - hb.writeElementOpen("p") - hb.writeEscaped("ID: ") - hb.writeEscaped(fmt.Sprintf("%d", c.ID)) - hb.writeElementOpen("br") - hb.writeEscaped("Target: ") - hb.writeElementOpen("a", "href", c.Target, "target", "_blank") - hb.writeEscaped(c.Target) - hb.writeElementClose("a") - hb.writeElementOpen("br") - hb.writeEscaped("Name: ") + hb.WriteElementOpen("p") + hb.WriteEscaped("ID: ") + hb.WriteEscaped(fmt.Sprintf("%d", c.ID)) + hb.WriteElementOpen("br") + hb.WriteEscaped("Target: ") + hb.WriteElementOpen("a", "href", c.Target, "target", "_blank") + hb.WriteEscaped(c.Target) + hb.WriteElementClose("a") + hb.WriteElementOpen("br") + hb.WriteEscaped("Name: ") if c.Website != "" { - hb.writeElementOpen("a", "href", c.Website, "target", "_blank", "rel", "nofollow noopener noreferrer ugc") + hb.WriteElementOpen("a", "href", c.Website, "target", "_blank", "rel", "nofollow noopener noreferrer ugc") } - hb.writeEscaped(c.Name) + hb.WriteEscaped(c.Name) if c.Website != "" { - hb.writeElementClose("a") + hb.WriteElementClose("a") } - hb.writeElementClose("p") + hb.WriteElementClose("p") // Comment - hb.writeElementOpen("p") - hb.write(c.Comment) - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteUnescaped(c.Comment) + hb.WriteElementClose("p") // Delete form - hb.writeElementOpen("form", "class", "actions", "method", "post", "action", rd.Blog.getRelativePath("/comment/delete")) - hb.writeElementOpen("input", "type", "hidden", "name", "commentid", "value", c.ID) - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete")) - hb.writeElementClose("form") - hb.writeElementClose("div") + hb.WriteElementOpen("form", "class", "actions", "method", "post", "action", rd.Blog.getRelativePath("/comment/delete")) + hb.WriteElementOpen("input", "type", "hidden", "name", "commentid", "value", c.ID) + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete")) + hb.WriteElementClose("form") + hb.WriteElementClose("div") } // Pagination a.renderPagination(hb, rd.Blog, crd.hasPrev, crd.hasNext, crd.prev, crd.next) - hb.writeElementClose("main") + hb.WriteElementClose("main") }, ) } @@ -1246,89 +1273,89 @@ type webmentionRenderData struct { prev, current, next string } -func (a *goBlog) renderWebmentionAdmin(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderWebmentionAdmin(hb *htmlbuilder.HtmlBuilder, rd *renderData) { wrd, ok := rd.Data.(*webmentionRenderData) if !ok { return } a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, a.ts.GetTemplateStringVariant(rd.Blog.Lang, "webmentions")) }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main") // Title - hb.writeElementOpen("h1") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "webmentions")) - hb.writeElementClose("h1") + hb.WriteElementOpen("h1") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "webmentions")) + hb.WriteElementClose("h1") // Notifications tdLocale := matchTimeDiffLocale(rd.Blog.Lang) for _, m := range wrd.mentions { - hb.writeElementOpen("div", "id", fmt.Sprintf("mention-%d", m.ID), "class", "p") - hb.writeElementOpen("p") + hb.WriteElementOpen("div", "id", fmt.Sprintf("mention-%d", m.ID), "class", "p") + hb.WriteElementOpen("p") // Source - hb.writeEscaped("From: ") - hb.writeElementOpen("a", "href", m.Source, "target", "_blank", "rel", "noopener noreferrer") - hb.writeEscaped(m.Source) - hb.writeElementClose("a") - hb.writeElementOpen("br") + hb.WriteEscaped("From: ") + hb.WriteElementOpen("a", "href", m.Source, "target", "_blank", "rel", "noopener noreferrer") + hb.WriteEscaped(m.Source) + hb.WriteElementClose("a") + hb.WriteElementOpen("br") // u-url if m.Source != m.Url { - hb.writeEscaped("u-url: ") - hb.writeElementOpen("a", "href", m.Url, "target", "_blank", "rel", "noopener noreferrer") - hb.writeEscaped(m.Url) - hb.writeElementClose("a") - hb.writeElementOpen("br") + hb.WriteEscaped("u-url: ") + hb.WriteElementOpen("a", "href", m.Url, "target", "_blank", "rel", "noopener noreferrer") + hb.WriteEscaped(m.Url) + hb.WriteElementClose("a") + hb.WriteElementOpen("br") } // Target - hb.writeEscaped("To: ") - hb.writeElementOpen("a", "href", m.Target, "target", "_blank") - hb.writeEscaped(m.Target) - hb.writeElementClose("a") - hb.writeElementOpen("br") + hb.WriteEscaped("To: ") + hb.WriteElementOpen("a", "href", m.Target, "target", "_blank") + hb.WriteEscaped(m.Target) + hb.WriteElementClose("a") + hb.WriteElementOpen("br") // Date - hb.writeEscaped("Created: ") - hb.writeEscaped(timediff.TimeDiff(time.Unix(m.Created, 0), timediff.WithLocale(tdLocale))) - hb.writeElementOpen("br") - hb.writeElementOpen("br") + hb.WriteEscaped("Created: ") + hb.WriteEscaped(timediff.TimeDiff(time.Unix(m.Created, 0), timediff.WithLocale(tdLocale))) + hb.WriteElementOpen("br") + hb.WriteElementOpen("br") // Author if m.Author != "" { - hb.writeEscaped(m.Author) - hb.writeElementOpen("br") + hb.WriteEscaped(m.Author) + hb.WriteElementOpen("br") } // Title if m.Title != "" { - hb.writeElementOpen("strong") - hb.writeEscaped(m.Title) - hb.writeElementClose("strong") - hb.writeElementOpen("br") + hb.WriteElementOpen("strong") + hb.WriteEscaped(m.Title) + hb.WriteElementClose("strong") + hb.WriteElementOpen("br") } // Content if m.Content != "" { - hb.writeElementOpen("i") - hb.writeEscaped(m.Content) - hb.writeElementClose("i") - hb.writeElementOpen("br") + hb.WriteElementOpen("i") + hb.WriteEscaped(m.Content) + hb.WriteElementClose("i") + hb.WriteElementOpen("br") } - hb.writeElementClose("p") + hb.WriteElementClose("p") // Actions - hb.writeElementOpen("form", "method", "post", "class", "actions") - hb.writeElementOpen("input", "type", "hidden", "name", "mentionid", "value", m.ID) - hb.writeElementOpen("input", "type", "hidden", "name", "redir", "value", fmt.Sprintf("%s#mention-%d", wrd.current, m.ID)) + hb.WriteElementOpen("form", "method", "post", "class", "actions") + hb.WriteElementOpen("input", "type", "hidden", "name", "mentionid", "value", m.ID) + hb.WriteElementOpen("input", "type", "hidden", "name", "redir", "value", fmt.Sprintf("%s#mention-%d", wrd.current, m.ID)) if m.Status == webmentionStatusVerified { // Approve verified mention - hb.writeElementOpen("input", "type", "submit", "formaction", "/webmention/approve", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "approve")) + hb.WriteElementOpen("input", "type", "submit", "formaction", "/webmention/approve", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "approve")) } // Delete mention - hb.writeElementOpen("input", "type", "submit", "formaction", "/webmention/delete", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete")) + hb.WriteElementOpen("input", "type", "submit", "formaction", "/webmention/delete", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete")) // Reverify mention - hb.writeElementOpen("input", "type", "submit", "formaction", "/webmention/reverify", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "reverify")) - hb.writeElementClose("form") + hb.WriteElementOpen("input", "type", "submit", "formaction", "/webmention/reverify", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "reverify")) + hb.WriteElementClose("form") } // Pagination a.renderPagination(hb, rd.Blog, wrd.hasPrev, wrd.hasNext, wrd.prev, wrd.next) - hb.writeElementClose("main") + hb.WriteElementClose("main") }, ) } @@ -1338,33 +1365,33 @@ type editorRenderData struct { updatePostContent string } -func (a *goBlog) renderEditor(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderEditor(hb *htmlbuilder.HtmlBuilder, rd *renderData) { edrd, ok := rd.Data.(*editorRenderData) if !ok { return } a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, a.ts.GetTemplateStringVariant(rd.Blog.Lang, "editor")) // Chroma CSS - hb.writeElementOpen("link", "rel", "stylesheet", "href", a.assetFileName("css/chroma.css")) + hb.WriteElementOpen("link", "rel", "stylesheet", "href", a.assetFileName("css/chroma.css")) }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main") // Title - hb.writeElementOpen("h1") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "editor")) - hb.writeElementClose("h1") + hb.WriteElementOpen("h1") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "editor")) + hb.WriteElementClose("h1") // Create - hb.writeElementOpen("h2") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "create")) - hb.writeElementClose("h2") + hb.WriteElementOpen("h2") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "create")) + hb.WriteElementClose("h2") _ = a.renderMarkdownToWriter(hb, a.editorPostDesc(rd.Blog), false) - hb.writeElementOpen("form", "method", "post", "class", "fw p") - hb.writeElementOpen("input", "type", "hidden", "name", "h", "value", "entry") - hb.writeElementOpen( + hb.WriteElementOpen("form", "method", "post", "class", "fw p") + hb.WriteElementOpen("input", "type", "hidden", "name", "h", "value", "entry") + hb.WriteElementOpen( "textarea", "name", "content", "class", "monospace h400p formcache mdpreview", @@ -1372,47 +1399,47 @@ func (a *goBlog) renderEditor(hb *htmlBuilder, rd *renderData) { "data-preview", "post-preview", "data-previewws", rd.Blog.getRelativePath("/editor/preview"), ) - hb.writeEscaped(a.editorPostTemplate(rd.BlogString, rd.Blog)) - hb.writeElementClose("textarea") - hb.writeElementOpen("div", "id", "post-preview", "class", "hide") - hb.writeElementClose("div") - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "create")) - hb.writeElementClose("form") + hb.WriteEscaped(a.editorPostTemplate(rd.BlogString, rd.Blog)) + hb.WriteElementClose("textarea") + hb.WriteElementOpen("div", "id", "post-preview", "class", "hide") + hb.WriteElementClose("div") + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "create")) + hb.WriteElementClose("form") // Update if edrd.updatePostUrl != "" { - hb.writeElementOpen("h2", "id", "update") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update")) - hb.writeElementClose("h2") - hb.writeElementOpen("form", "method", "post", "class", "fw p", "action", "#update") - hb.writeElementOpen("input", "type", "hidden", "name", "editoraction", "value", "updatepost") - hb.writeElementOpen("input", "type", "hidden", "name", "url", "value", edrd.updatePostUrl) - hb.writeElementOpen( + hb.WriteElementOpen("h2", "id", "update") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update")) + hb.WriteElementClose("h2") + hb.WriteElementOpen("form", "method", "post", "class", "fw p", "action", "#update") + hb.WriteElementOpen("input", "type", "hidden", "name", "editoraction", "value", "updatepost") + hb.WriteElementOpen("input", "type", "hidden", "name", "url", "value", edrd.updatePostUrl) + hb.WriteElementOpen( "textarea", "name", "content", "class", "monospace h400p mdpreview", "data-preview", "update-preview", "data-previewws", rd.Blog.getRelativePath("/editor/preview"), ) - hb.writeEscaped(edrd.updatePostContent) - hb.writeElementClose("textarea") - hb.writeElementOpen("div", "id", "update-preview", "class", "hide") - hb.writeElementClose("div") - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update")) - hb.writeElementClose("form") + hb.WriteEscaped(edrd.updatePostContent) + hb.WriteElementClose("textarea") + hb.WriteElementOpen("div", "id", "update-preview", "class", "hide") + hb.WriteElementClose("div") + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update")) + hb.WriteElementClose("form") } // Posts - hb.writeElementOpen("h2") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "posts")) - hb.writeElementClose("h2") + hb.WriteElementOpen("h2") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "posts")) + hb.WriteElementClose("h2") // Template postsListLink := func(path, title string) { - hb.writeElementOpen("p") - hb.writeElementOpen("a", "href", rd.Blog.getRelativePath(path)) - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, title)) - hb.writeElementClose("a") - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteElementOpen("a", "href", rd.Blog.getRelativePath(path)) + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, title)) + hb.WriteElementClose("a") + hb.WriteElementClose("p") } // Drafts postsListLink("/editor/drafts", "drafts") @@ -1426,54 +1453,54 @@ func (a *goBlog) renderEditor(hb *htmlBuilder, rd *renderData) { postsListLink("/editor/deleted", "deletedposts") // Upload - hb.writeElementOpen("h2") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "upload")) - hb.writeElementClose("h2") - hb.writeElementOpen("form", "class", "fw p", "method", "post", "enctype", "multipart/form-data") - hb.writeElementOpen("input", "type", "hidden", "name", "editoraction", "value", "upload") - hb.writeElementOpen("input", "type", "file", "name", "file") - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "upload")) - hb.writeElementClose("form") + hb.WriteElementOpen("h2") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "upload")) + hb.WriteElementClose("h2") + hb.WriteElementOpen("form", "class", "fw p", "method", "post", "enctype", "multipart/form-data") + hb.WriteElementOpen("input", "type", "hidden", "name", "editoraction", "value", "upload") + hb.WriteElementOpen("input", "type", "file", "name", "file") + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "upload")) + hb.WriteElementClose("form") // Media files - hb.writeElementOpen("p") - hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/editor/files")) - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "mediafiles")) - hb.writeElementClose("a") - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteElementOpen("a", "href", rd.Blog.getRelativePath("/editor/files")) + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "mediafiles")) + hb.WriteElementClose("a") + hb.WriteElementClose("p") // Location-Helper - hb.writeElementOpen("h2") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "location")) - hb.writeElementClose("h2") - hb.writeElementOpen("form", "class", "fw p") - hb.writeElementOpen( + hb.WriteElementOpen("h2") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "location")) + hb.WriteElementClose("h2") + hb.WriteElementOpen("form", "class", "fw p") + hb.WriteElementOpen( "input", "id", "geobtn", "type", "button", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "locationget"), "data-failed", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "locationfailed"), "data-notsupported", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "locationnotsupported"), ) - hb.writeElementOpen("input", "id", "geostatus", "type", "text", "class", "hide", "readonly", "") - hb.writeElementClose("form") + hb.WriteElementOpen("input", "id", "geostatus", "type", "text", "class", "hide", "readonly", "") + hb.WriteElementClose("form") // GPX-Helper - hb.writeElementOpen("h2") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "gpxhelper")) - hb.writeElementClose("h2") - hb.writeElementOpen("p") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "gpxhelperdesc")) - hb.writeElementClose("p") - hb.writeElementOpen("form", "class", "fw p", "method", "post", "enctype", "multipart/form-data") - hb.writeElementOpen("input", "type", "hidden", "name", "editoraction", "value", "helpgpx") - hb.writeElementOpen("input", "type", "file", "name", "file") - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "upload")) - hb.writeElementClose("form") + hb.WriteElementOpen("h2") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "gpxhelper")) + hb.WriteElementClose("h2") + hb.WriteElementOpen("p") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "gpxhelperdesc")) + hb.WriteElementClose("p") + hb.WriteElementOpen("form", "class", "fw p", "method", "post", "enctype", "multipart/form-data") + hb.WriteElementOpen("input", "type", "hidden", "name", "editoraction", "value", "helpgpx") + hb.WriteElementOpen("input", "type", "file", "name", "file") + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "upload")) + hb.WriteElementClose("form") - hb.writeElementClose("main") + hb.WriteElementClose("main") // Scripts for _, script := range []string{"js/mdpreview.js", "js/geohelper.js", "js/formcache.js"} { - hb.writeElementOpen("script", "src", a.assetFileName(script), "defer", "") - hb.writeElementClose("script") + hb.WriteElementOpen("script", "src", a.assetFileName(script), "defer", "") + hb.WriteElementClose("script") } }, ) @@ -1486,28 +1513,28 @@ type settingsRenderData struct { hideOldContentWarning bool } -func (a *goBlog) renderSettings(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderSettings(hb *htmlbuilder.HtmlBuilder, rd *renderData) { srd, ok := rd.Data.(*settingsRenderData) if !ok { return } a.renderBase( hb, rd, - func(hb *htmlBuilder) { + func(hb *htmlbuilder.HtmlBuilder) { a.renderTitleTag(hb, rd.Blog, a.ts.GetTemplateStringVariant(rd.Blog.Lang, "settings")) }, - func(hb *htmlBuilder) { - hb.writeElementOpen("main") + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main") // Title - hb.writeElementOpen("h1") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "settings")) - hb.writeElementClose("h1") + hb.WriteElementOpen("h1") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "settings")) + hb.WriteElementClose("h1") // General - hb.writeElementOpen("h2") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "general")) - hb.writeElementClose("h2") + hb.WriteElementOpen("h2") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "general")) + hb.WriteElementClose("h2") // Hide old content warning a.renderCollapsibleBooleanSetting(hb, rd, @@ -1522,10 +1549,10 @@ func (a *goBlog) renderSettings(hb *htmlBuilder, rd *renderData) { a.renderPostSectionSettings(hb, rd, srd) // Scripts - hb.writeElementOpen("script", "src", a.assetFileName("js/formconfirm.js"), "defer", "") - hb.writeElementClose("script") + hb.WriteElementOpen("script", "src", a.assetFileName("js/formconfirm.js"), "defer", "") + hb.WriteElementClose("script") - hb.writeElementClose("main") + hb.WriteElementClose("main") }, ) } diff --git a/uiComponents.go b/uiComponents.go index bf4f480..ebf35b8 100644 --- a/uiComponents.go +++ b/uiComponents.go @@ -7,6 +7,7 @@ import ( "github.com/samber/lo" "go.goblog.app/app/pkgs/bufferpool" + "go.goblog.app/app/pkgs/htmlbuilder" ) type summaryTyp string @@ -17,7 +18,7 @@ const ( ) // post summary on index pages -func (a *goBlog) renderSummary(hb *htmlBuilder, bc *configBlog, p *post, typ summaryTyp) { +func (a *goBlog) renderSummary(hb *htmlbuilder.HtmlBuilder, bc *configBlog, p *post, typ summaryTyp) { if bc == nil || p == nil { return } @@ -25,21 +26,21 @@ func (a *goBlog) renderSummary(hb *htmlBuilder, bc *configBlog, p *post, typ sum typ = defaultSummary } // Start article - hb.writeElementOpen("article", "class", "h-entry border-bottom") + hb.WriteElementOpen("article", "class", "h-entry border-bottom") if p.Priority > 0 { // Is pinned post - hb.writeElementOpen("p") - hb.writeEscaped("📌 ") - hb.writeEscaped(a.ts.GetTemplateStringVariant(bc.Lang, "pinned")) - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteEscaped("📌 ") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(bc.Lang, "pinned")) + hb.WriteElementClose("p") } if p.RenderedTitle != "" { // Has title - hb.writeElementOpen("h2", "class", "p-name") - hb.writeElementOpen("a", "class", "u-url", "href", p.Path) - hb.writeEscaped(p.RenderedTitle) - hb.writeElementClose("a") - hb.writeElementClose("h2") + hb.WriteElementOpen("h2", "class", "p-name") + hb.WriteElementOpen("a", "class", "u-url", "href", p.Path) + hb.WriteEscaped(p.RenderedTitle) + hb.WriteElementClose("a") + hb.WriteElementClose("h2") } // Show photos in photo summary photos := a.photoLinks(p) @@ -52,17 +53,17 @@ func (a *goBlog) renderSummary(hb *htmlBuilder, bc *configBlog, p *post, typ sum a.renderPostMeta(hb, p, bc, "summary") if typ != photoSummary && a.showFull(p) { // Show full content - hb.writeElementOpen("div", "class", "e-content") + hb.WriteElementOpen("div", "class", "e-content") a.postHtmlToWriter(hb, p, false) - hb.writeElementClose("div") + hb.WriteElementClose("div") } else { // Show summary - hb.writeElementOpen("p", "class", "p-summary") - hb.writeEscaped(a.postSummary(p)) - hb.writeElementClose("p") + hb.WriteElementOpen("p", "class", "p-summary") + hb.WriteEscaped(a.postSummary(p)) + hb.WriteElementClose("p") } // Show link to full post - hb.writeElementOpen("p") + hb.WriteElementOpen("p") prefix := bufferpool.Get() if len(photos) > 0 { // Contains photos @@ -74,19 +75,19 @@ func (a *goBlog) renderSummary(hb *htmlBuilder, bc *configBlog, p *post, typ sum } if prefix.Len() > 0 { prefix.WriteRune(' ') - hb.writeEscaped(prefix.String()) + hb.WriteEscaped(prefix.String()) } bufferpool.Put(prefix) - hb.writeElementOpen("a", "class", "u-url", "href", p.Path) - hb.writeEscaped(a.ts.GetTemplateStringVariant(bc.Lang, "view")) - hb.writeElementClose("a") - hb.writeElementClose("p") + hb.WriteElementOpen("a", "class", "u-url", "href", p.Path) + hb.WriteEscaped(a.ts.GetTemplateStringVariant(bc.Lang, "view")) + hb.WriteElementClose("a") + hb.WriteElementClose("p") // Finish article - hb.writeElementClose("article") + hb.WriteElementClose("article") } // list of post taxonomy values (tags, series, etc.) -func (a *goBlog) renderPostTax(hb *htmlBuilder, p *post, b *configBlog) { +func (a *goBlog) renderPostTax(hb *htmlbuilder.HtmlBuilder, p *post, b *configBlog) { if b == nil || p == nil { return } @@ -95,372 +96,372 @@ func (a *goBlog) renderPostTax(hb *htmlBuilder, p *post, b *configBlog) { // Get all sorted taxonomy values for this post if taxValues := sortedStrings(p.Parameters[tax.Name]); len(taxValues) > 0 { // Start new paragraph - hb.writeElementOpen("p") + hb.WriteElementOpen("p") // Add taxonomy name - hb.writeElementOpen("strong") - hb.writeEscaped(a.renderMdTitle(tax.Title)) - hb.writeElementClose("strong") - hb.write(": ") + hb.WriteElementOpen("strong") + hb.WriteEscaped(a.renderMdTitle(tax.Title)) + hb.WriteElementClose("strong") + hb.WriteUnescaped(": ") // Add taxonomy values for i, taxValue := range taxValues { if i > 0 { - hb.write(", ") + hb.WriteUnescaped(", ") } - hb.writeElementOpen( + hb.WriteElementOpen( "a", "class", "p-category", "rel", "tag", "href", b.getRelativePath(fmt.Sprintf("/%s/%s", tax.Name, urlize(taxValue))), ) - hb.writeEscaped(a.renderMdTitle(taxValue)) - hb.writeElementClose("a") + hb.WriteEscaped(a.renderMdTitle(taxValue)) + hb.WriteElementClose("a") } // End paragraph - hb.writeElementClose("p") + hb.WriteElementClose("p") } } } // post meta information. // typ can be "summary", "post" or "preview". -func (a *goBlog) renderPostMeta(hb *htmlBuilder, p *post, b *configBlog, typ string) { +func (a *goBlog) renderPostMeta(hb *htmlbuilder.HtmlBuilder, p *post, b *configBlog, typ string) { if b == nil || p == nil || typ != "summary" && typ != "post" && typ != "preview" { return } if typ == "summary" || typ == "post" { - hb.writeElementOpen("div", "class", "p") + hb.WriteElementOpen("div", "class", "p") } // Published time if published := toLocalTime(p.Published); !published.IsZero() { - hb.writeElementOpen("div") - hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "publishedon")) - hb.write(" ") - hb.writeElementOpen("time", "class", "dt-published", "datetime", published.Format(time.RFC3339)) - hb.writeEscaped(published.Format(isoDateFormat)) - hb.writeElementClose("time") + hb.WriteElementOpen("div") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "publishedon")) + hb.WriteUnescaped(" ") + hb.WriteElementOpen("time", "class", "dt-published", "datetime", published.Format(time.RFC3339)) + hb.WriteEscaped(published.Format(isoDateFormat)) + hb.WriteElementClose("time") // Section if p.Section != "" { if section := b.Sections[p.Section]; section != nil { - hb.write(" in ") // TODO: Replace with a proper translation - hb.writeElementOpen("a", "href", b.getRelativePath(section.Name)) - hb.writeEscaped(a.renderMdTitle(section.Title)) - hb.writeElementClose("a") + hb.WriteUnescaped(" in ") // TODO: Replace with a proper translation + hb.WriteElementOpen("a", "href", b.getRelativePath(section.Name)) + hb.WriteEscaped(a.renderMdTitle(section.Title)) + hb.WriteElementClose("a") } } - hb.writeElementClose("div") + hb.WriteElementClose("div") } // Updated time if updated := toLocalTime(p.Updated); !updated.IsZero() { - hb.writeElementOpen("div") - hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "updatedon")) - hb.write(" ") - hb.writeElementOpen("time", "class", "dt-updated", "datetime", updated.Format(time.RFC3339)) - hb.writeEscaped(updated.Format(isoDateFormat)) - hb.writeElementClose("time") - hb.writeElementClose("div") + hb.WriteElementOpen("div") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "updatedon")) + hb.WriteUnescaped(" ") + hb.WriteElementOpen("time", "class", "dt-updated", "datetime", updated.Format(time.RFC3339)) + hb.WriteEscaped(updated.Format(isoDateFormat)) + hb.WriteElementClose("time") + hb.WriteElementClose("div") } // IndieWeb Meta a.renderPostReplyContext(hb, p, "") a.renderPostLikeContext(hb, p, "") // Like ("u-like-of") if likeLink := a.likeLink(p); likeLink != "" { - hb.writeElementOpen("div") - hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "likeof")) - hb.writeEscaped(": ") - hb.writeElementOpen("a", "class", "u-like-of", "rel", "noopener", "target", "_blank", "href", likeLink) + hb.WriteElementOpen("div") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "likeof")) + hb.WriteEscaped(": ") + hb.WriteElementOpen("a", "class", "u-like-of", "rel", "noopener", "target", "_blank", "href", likeLink) if likeTitle := a.likeTitle(p); likeTitle != "" { - hb.writeEscaped(likeTitle) + hb.WriteEscaped(likeTitle) } else { - hb.writeEscaped(likeLink) + hb.WriteEscaped(likeLink) } - hb.writeElementClose("a") - hb.writeElementClose("div") + hb.WriteElementClose("a") + hb.WriteElementClose("div") } // Geo if geoURIs := a.geoURIs(p); len(geoURIs) != 0 { - hb.writeElementOpen("div") - hb.writeEscaped("📍 ") + hb.WriteElementOpen("div") + hb.WriteEscaped("📍 ") for i, geoURI := range geoURIs { if i > 0 { - hb.writeEscaped(", ") + hb.WriteEscaped(", ") } - hb.writeElementOpen("a", "class", "p-location h-geo", "target", "_blank", "rel", "nofollow noopener noreferrer", "href", geoOSMLink(geoURI)) - hb.writeElementOpen("span", "class", "p-name") - hb.writeEscaped(a.geoTitle(geoURI, b.Lang)) - hb.writeElementClose("span") - hb.writeElementOpen("data", "class", "p-longitude", "value", fmt.Sprintf("%f", geoURI.Longitude)) - hb.writeElementClose("data") - hb.writeElementOpen("data", "class", "p-latitude", "value", fmt.Sprintf("%f", geoURI.Latitude)) - hb.writeElementClose("data") - hb.writeElementClose("a") + hb.WriteElementOpen("a", "class", "p-location h-geo", "target", "_blank", "rel", "nofollow noopener noreferrer", "href", geoOSMLink(geoURI)) + hb.WriteElementOpen("span", "class", "p-name") + hb.WriteEscaped(a.geoTitle(geoURI, b.Lang)) + hb.WriteElementClose("span") + hb.WriteElementOpen("data", "class", "p-longitude", "value", fmt.Sprintf("%f", geoURI.Longitude)) + hb.WriteElementClose("data") + hb.WriteElementOpen("data", "class", "p-latitude", "value", fmt.Sprintf("%f", geoURI.Latitude)) + hb.WriteElementClose("data") + hb.WriteElementClose("a") } - hb.writeElementClose("div") + hb.WriteElementClose("div") } // Post specific elements if typ == "post" { // Translations if translations := a.postTranslations(p); len(translations) > 0 { - hb.writeElementOpen("div") - hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "translations")) - hb.writeEscaped(": ") + hb.WriteElementOpen("div") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "translations")) + hb.WriteEscaped(": ") for i, translation := range translations { if i > 0 { - hb.writeEscaped(", ") + hb.WriteEscaped(", ") } - hb.writeElementOpen("a", "translate", "no", "href", translation.Path) - hb.writeEscaped(translation.RenderedTitle) - hb.writeElementClose("a") + hb.WriteElementOpen("a", "translate", "no", "href", translation.Path) + hb.WriteEscaped(translation.RenderedTitle) + hb.WriteElementClose("a") } - hb.writeElementClose("div") + hb.WriteElementClose("div") } // Short link if shortLink := a.shortPostURL(p); shortLink != "" { - hb.writeElementOpen("div") - hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "shorturl")) - hb.writeEscaped(" ") - hb.writeElementOpen("a", "rel", "shortlink", "href", shortLink) - hb.writeEscaped(shortLink) - hb.writeElementClose("a") - hb.writeElementClose("div") + hb.WriteElementOpen("div") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "shorturl")) + hb.WriteEscaped(" ") + hb.WriteElementOpen("a", "rel", "shortlink", "href", shortLink) + hb.WriteEscaped(shortLink) + hb.WriteElementClose("a") + hb.WriteElementClose("div") } // Status if p.Status != statusPublished { - hb.writeElementOpen("div") - hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "status")) - hb.writeEscaped(": ") - hb.writeEscaped(string(p.Status)) - hb.writeElementClose("div") + hb.WriteElementOpen("div") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "status")) + hb.WriteEscaped(": ") + hb.WriteEscaped(string(p.Status)) + hb.WriteElementClose("div") } } if typ == "summary" || typ == "post" { - hb.writeElementClose("div") + hb.WriteElementClose("div") } } // Reply ("u-in-reply-to") -func (a *goBlog) renderPostReplyContext(hb *htmlBuilder, p *post, htmlWrapperElement string) { +func (a *goBlog) renderPostReplyContext(hb *htmlbuilder.HtmlBuilder, p *post, htmlWrapperElement string) { if htmlWrapperElement == "" { htmlWrapperElement = "div" } if replyLink := a.replyLink(p); replyLink != "" { - hb.writeElementOpen(htmlWrapperElement) - hb.writeEscaped(a.ts.GetTemplateStringVariant(a.getBlogFromPost(p).Lang, "replyto")) - hb.writeEscaped(": ") - hb.writeElementOpen("a", "class", "u-in-reply-to", "rel", "noopener", "target", "_blank", "href", replyLink) + hb.WriteElementOpen(htmlWrapperElement) + hb.WriteEscaped(a.ts.GetTemplateStringVariant(a.getBlogFromPost(p).Lang, "replyto")) + hb.WriteEscaped(": ") + hb.WriteElementOpen("a", "class", "u-in-reply-to", "rel", "noopener", "target", "_blank", "href", replyLink) if replyTitle := a.replyTitle(p); replyTitle != "" { - hb.writeEscaped(replyTitle) + hb.WriteEscaped(replyTitle) } else { - hb.writeEscaped(replyLink) + hb.WriteEscaped(replyLink) } - hb.writeElementClose("a") - hb.writeElementClose(htmlWrapperElement) + hb.WriteElementClose("a") + hb.WriteElementClose(htmlWrapperElement) } } // Like ("u-like-of") -func (a *goBlog) renderPostLikeContext(hb *htmlBuilder, p *post, htmlWrapperElement string) { +func (a *goBlog) renderPostLikeContext(hb *htmlbuilder.HtmlBuilder, p *post, htmlWrapperElement string) { if htmlWrapperElement == "" { htmlWrapperElement = "div" } if likeLink := a.likeLink(p); likeLink != "" { - hb.writeElementOpen(htmlWrapperElement) - hb.writeEscaped(a.ts.GetTemplateStringVariant(a.getBlogFromPost(p).Lang, "likeof")) - hb.writeEscaped(": ") - hb.writeElementOpen("a", "class", "u-like-of", "rel", "noopener", "target", "_blank", "href", likeLink) + hb.WriteElementOpen(htmlWrapperElement) + hb.WriteEscaped(a.ts.GetTemplateStringVariant(a.getBlogFromPost(p).Lang, "likeof")) + hb.WriteEscaped(": ") + hb.WriteElementOpen("a", "class", "u-like-of", "rel", "noopener", "target", "_blank", "href", likeLink) if likeTitle := a.likeTitle(p); likeTitle != "" { - hb.writeEscaped(likeTitle) + hb.WriteEscaped(likeTitle) } else { - hb.writeEscaped(likeLink) + hb.WriteEscaped(likeLink) } - hb.writeElementClose("a") - hb.writeElementClose(htmlWrapperElement) + hb.WriteElementClose("a") + hb.WriteElementClose(htmlWrapperElement) } } // warning for old posts -func (a *goBlog) renderOldContentWarning(hb *htmlBuilder, p *post, b *configBlog) { +func (a *goBlog) renderOldContentWarning(hb *htmlbuilder.HtmlBuilder, p *post, b *configBlog) { if b == nil || b.hideOldContentWarning || p == nil || !p.Old() { return } - hb.writeElementOpen("strong", "class", "p border-top border-bottom") - hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "oldcontent")) - hb.writeElementClose("strong") + hb.WriteElementOpen("strong", "class", "p border-top border-bottom") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "oldcontent")) + hb.WriteElementClose("strong") } -func (a *goBlog) renderInteractions(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderInteractions(hb *htmlbuilder.HtmlBuilder, rd *renderData) { // Start accordion - hb.writeElementOpen("details", "class", "p", "id", "interactions") - hb.writeElementOpen("summary") - hb.writeElementOpen("strong") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "interactions")) - hb.writeElementClose("strong") - hb.writeElementClose("summary") + hb.WriteElementOpen("details", "class", "p", "id", "interactions") + hb.WriteElementOpen("summary") + hb.WriteElementOpen("strong") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "interactions")) + hb.WriteElementClose("strong") + hb.WriteElementClose("summary") // Render mentions var renderMentions func(m []*mention) renderMentions = func(m []*mention) { if len(m) == 0 { return } - hb.writeElementOpen("ul") + hb.WriteElementOpen("ul") for _, mention := range m { - hb.writeElementOpen("li") - hb.writeElementOpen("a", "href", mention.Url, "target", "_blank", "rel", "nofollow noopener noreferrer ugc") - hb.writeEscaped(defaultIfEmpty(mention.Author, mention.Url)) - hb.writeElementClose("a") + hb.WriteElementOpen("li") + hb.WriteElementOpen("a", "href", mention.Url, "target", "_blank", "rel", "nofollow noopener noreferrer ugc") + hb.WriteEscaped(defaultIfEmpty(mention.Author, mention.Url)) + hb.WriteElementClose("a") if mention.Title != "" { - hb.write(" ") - hb.writeElementOpen("strong") - hb.writeEscaped(mention.Title) - hb.writeElementClose("strong") + hb.WriteUnescaped(" ") + hb.WriteElementOpen("strong") + hb.WriteEscaped(mention.Title) + hb.WriteElementClose("strong") } if mention.Content != "" { - hb.write(" ") - hb.writeElementOpen("i") - hb.writeEscaped(mention.Content) - hb.writeElementClose("i") + hb.WriteUnescaped(" ") + hb.WriteElementOpen("i") + hb.WriteEscaped(mention.Content) + hb.WriteElementClose("i") } if len(mention.Submentions) > 0 { renderMentions(mention.Submentions) } - hb.writeElementClose("li") + hb.WriteElementClose("li") } - hb.writeElementClose("ul") + hb.WriteElementClose("ul") } renderMentions(a.db.getWebmentionsByAddress(rd.Canonical)) // Show form to send a webmention - hb.writeElementOpen("form", "class", "fw p", "method", "post", "action", "/webmention") - hb.writeElementOpen("label", "for", "wm-source", "class", "p") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "interactionslabel")) - hb.writeElementClose("label") - hb.writeElementOpen("input", "id", "wm-source", "type", "url", "name", "source", "placeholder", "URL", "required", "") - hb.writeElementOpen("input", "type", "hidden", "name", "target", "value", rd.Canonical) - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "send")) - hb.writeElementClose("form") + hb.WriteElementOpen("form", "class", "fw p", "method", "post", "action", "/webmention") + hb.WriteElementOpen("label", "for", "wm-source", "class", "p") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "interactionslabel")) + hb.WriteElementClose("label") + hb.WriteElementOpen("input", "id", "wm-source", "type", "url", "name", "source", "placeholder", "URL", "required", "") + hb.WriteElementOpen("input", "type", "hidden", "name", "target", "value", rd.Canonical) + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "send")) + hb.WriteElementClose("form") // Show form to create a new comment - hb.writeElementOpen("form", "class", "fw p", "method", "post", "action", "/comment") - hb.writeElementOpen("input", "type", "hidden", "name", "target", "value", rd.Canonical) - hb.writeElementOpen("input", "type", "text", "name", "name", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "nameopt")) - hb.writeElementOpen("input", "type", "url", "name", "website", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "websiteopt")) - hb.writeElementOpen("textarea", "name", "comment", "required", "", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "comment")) - hb.writeElementClose("textarea") - hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "docomment")) - hb.writeElementClose("form") + hb.WriteElementOpen("form", "class", "fw p", "method", "post", "action", "/comment") + hb.WriteElementOpen("input", "type", "hidden", "name", "target", "value", rd.Canonical) + hb.WriteElementOpen("input", "type", "text", "name", "name", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "nameopt")) + hb.WriteElementOpen("input", "type", "url", "name", "website", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "websiteopt")) + hb.WriteElementOpen("textarea", "name", "comment", "required", "", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "comment")) + hb.WriteElementClose("textarea") + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "docomment")) + hb.WriteElementClose("form") // Finish accordion - hb.writeElementClose("details") + hb.WriteElementClose("details") } // author h-card -func (a *goBlog) renderAuthor(hb *htmlBuilder) { +func (a *goBlog) renderAuthor(hb *htmlbuilder.HtmlBuilder) { user := a.cfg.User if user == nil { return } - hb.writeElementOpen("div", "class", "p-author h-card hide") + hb.WriteElementOpen("div", "class", "p-author h-card hide") if user.Picture != "" { - hb.writeElementOpen("data", "class", "u-photo", "value", user.Picture) - hb.writeElementClose("data") + hb.WriteElementOpen("data", "class", "u-photo", "value", user.Picture) + hb.WriteElementClose("data") } if user.Name != "" { - hb.writeElementOpen("a", "class", "p-name u-url", "rel", "me", "href", defaultIfEmpty(user.Link, "/")) - hb.writeEscaped(user.Name) - hb.writeElementClose("a") + hb.WriteElementOpen("a", "class", "p-name u-url", "rel", "me", "href", defaultIfEmpty(user.Link, "/")) + hb.WriteEscaped(user.Name) + hb.WriteElementClose("a") } - hb.writeElementClose("div") + hb.WriteElementClose("div") } // head meta tags for a post -func (a *goBlog) renderPostHeadMeta(hb *htmlBuilder, p *post, canonical string) { +func (a *goBlog) renderPostHeadMeta(hb *htmlbuilder.HtmlBuilder, p *post, canonical string) { if p == nil { return } if canonical != "" { - hb.writeElementOpen("meta", "property", "og:url", "content", canonical) - hb.writeElementOpen("meta", "property", "twitter:url", "content", canonical) + hb.WriteElementOpen("meta", "property", "og:url", "content", canonical) + hb.WriteElementOpen("meta", "property", "twitter:url", "content", canonical) } if p.RenderedTitle != "" { - hb.writeElementOpen("meta", "property", "og:title", "content", p.RenderedTitle) - hb.writeElementOpen("meta", "property", "twitter:title", "content", p.RenderedTitle) + hb.WriteElementOpen("meta", "property", "og:title", "content", p.RenderedTitle) + hb.WriteElementOpen("meta", "property", "twitter:title", "content", p.RenderedTitle) } if summary := a.postSummary(p); summary != "" { - hb.writeElementOpen("meta", "name", "description", "content", summary) - hb.writeElementOpen("meta", "property", "og:description", "content", summary) - hb.writeElementOpen("meta", "property", "twitter:description", "content", summary) + hb.WriteElementOpen("meta", "name", "description", "content", summary) + hb.WriteElementOpen("meta", "property", "og:description", "content", summary) + hb.WriteElementOpen("meta", "property", "twitter:description", "content", summary) } if published := toLocalTime(p.Published); !published.IsZero() { - hb.writeElementOpen("meta", "itemprop", "datePublished", "content", published.Format(time.RFC3339)) + hb.WriteElementOpen("meta", "itemprop", "datePublished", "content", published.Format(time.RFC3339)) } if updated := toLocalTime(p.Updated); !updated.IsZero() { - hb.writeElementOpen("meta", "itemprop", "dateModified", "content", updated.Format(time.RFC3339)) + hb.WriteElementOpen("meta", "itemprop", "dateModified", "content", updated.Format(time.RFC3339)) } for _, img := range a.photoLinks(p) { - hb.writeElementOpen("meta", "itemprop", "image", "content", img) - hb.writeElementOpen("meta", "property", "og:image", "content", img) - hb.writeElementOpen("meta", "property", "twitter:image", "content", img) + hb.WriteElementOpen("meta", "itemprop", "image", "content", img) + hb.WriteElementOpen("meta", "property", "og:image", "content", img) + hb.WriteElementOpen("meta", "property", "twitter:image", "content", img) } } // TOR notice in the footer -func (a *goBlog) renderTorNotice(hb *htmlBuilder, rd *renderData) { +func (a *goBlog) renderTorNotice(hb *htmlbuilder.HtmlBuilder, rd *renderData) { if !a.cfg.Server.Tor || (!rd.TorUsed && rd.TorAddress == "") { return } if rd.TorUsed { - hb.writeElementOpen("p", "id", "tor") - hb.writeEscaped("🔐 ") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "connectedviator")) - hb.writeElementClose("p") + hb.WriteElementOpen("p", "id", "tor") + hb.WriteEscaped("🔐 ") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "connectedviator")) + hb.WriteElementClose("p") } else if rd.TorAddress != "" { - hb.writeElementOpen("p", "id", "tor") - hb.writeEscaped("🔓 ") - hb.writeElementOpen("a", "href", rd.TorAddress) - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "connectviator")) - hb.writeElementClose("a") - hb.writeEscaped(" ") - hb.writeElementOpen("a", "href", "https://www.torproject.org/", "target", "_blank", "rel", "nofollow noopener noreferrer") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "whatistor")) - hb.writeElementClose("a") - hb.writeElementClose("p") + hb.WriteElementOpen("p", "id", "tor") + hb.WriteEscaped("🔓 ") + hb.WriteElementOpen("a", "href", rd.TorAddress) + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "connectviator")) + hb.WriteElementClose("a") + hb.WriteEscaped(" ") + hb.WriteElementOpen("a", "href", "https://www.torproject.org/", "target", "_blank", "rel", "nofollow noopener noreferrer") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "whatistor")) + hb.WriteElementClose("a") + hb.WriteElementClose("p") } } -func (a *goBlog) renderTitleTag(hb *htmlBuilder, blog *configBlog, optionalTitle string) { - hb.writeElementOpen("title") +func (a *goBlog) renderTitleTag(hb *htmlbuilder.HtmlBuilder, blog *configBlog, optionalTitle string) { + hb.WriteElementOpen("title") if optionalTitle != "" { - hb.writeEscaped(optionalTitle) - hb.writeEscaped(" - ") + hb.WriteEscaped(optionalTitle) + hb.WriteEscaped(" - ") } - hb.writeEscaped(a.renderMdTitle(blog.Title)) - hb.writeElementClose("title") + hb.WriteEscaped(a.renderMdTitle(blog.Title)) + hb.WriteElementClose("title") } -func (a *goBlog) renderPagination(hb *htmlBuilder, blog *configBlog, hasPrev, hasNext bool, prev, next string) { +func (a *goBlog) renderPagination(hb *htmlbuilder.HtmlBuilder, blog *configBlog, hasPrev, hasNext bool, prev, next string) { // Navigation if hasPrev { - hb.writeElementOpen("p") - hb.writeElementOpen("a", "href", prev) // TODO: rel=prev? - hb.writeEscaped(a.ts.GetTemplateStringVariant(blog.Lang, "prev")) - hb.writeElementClose("a") - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteElementOpen("a", "href", prev) // TODO: rel=prev? + hb.WriteEscaped(a.ts.GetTemplateStringVariant(blog.Lang, "prev")) + hb.WriteElementClose("a") + hb.WriteElementClose("p") } if hasNext { - hb.writeElementOpen("p") - hb.writeElementOpen("a", "href", next) // TODO: rel=next? - hb.writeEscaped(a.ts.GetTemplateStringVariant(blog.Lang, "next")) - hb.writeElementClose("a") - hb.writeElementClose("p") + hb.WriteElementOpen("p") + hb.WriteElementOpen("a", "href", next) // TODO: rel=next? + hb.WriteEscaped(a.ts.GetTemplateStringVariant(blog.Lang, "next")) + hb.WriteElementClose("a") + hb.WriteElementClose("p") } } -func (*goBlog) renderPostTitle(hb *htmlBuilder, p *post) { +func (*goBlog) renderPostTitle(hb *htmlbuilder.HtmlBuilder, p *post) { if p == nil || p.RenderedTitle == "" { return } - hb.writeElementOpen("h1", "class", "p-name") - hb.writeEscaped(p.RenderedTitle) - hb.writeElementClose("h1") + hb.WriteElementOpen("h1", "class", "p-name") + hb.WriteEscaped(p.RenderedTitle) + hb.WriteElementClose("h1") } -func (a *goBlog) renderPostGPX(hb *htmlBuilder, p *post, b *configBlog) { +func (a *goBlog) renderPostGPX(hb *htmlbuilder.HtmlBuilder, p *post, b *configBlog) { if p == nil || !p.hasTrack() { return } @@ -469,174 +470,174 @@ func (a *goBlog) renderPostGPX(hb *htmlBuilder, p *post, b *configBlog) { return } // Track stats - hb.writeElementOpen("p") + hb.WriteElementOpen("p") if track.Name != "" { - hb.writeElementOpen("strong") - hb.writeEscaped(track.Name) - hb.writeElementClose("strong") - hb.write(" ") + hb.WriteElementOpen("strong") + hb.WriteEscaped(track.Name) + hb.WriteElementClose("strong") + hb.WriteUnescaped(" ") } if track.Kilometers != "" { - hb.write("🏁 ") - hb.writeEscaped(track.Kilometers) - hb.write(" ") - hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "kilometers")) - hb.write(" ") + hb.WriteUnescaped("🏁 ") + hb.WriteEscaped(track.Kilometers) + hb.WriteUnescaped(" ") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "kilometers")) + hb.WriteUnescaped(" ") } if track.Hours != "" { - hb.write("⏱ ") - hb.writeEscaped(track.Hours) + hb.WriteUnescaped("⏱ ") + hb.WriteEscaped(track.Hours) } - hb.writeElementClose("p") + hb.WriteElementClose("p") // Map (only show if it has features) if track.hasMapFeatures() { - hb.writeElementOpen( + hb.WriteElementOpen( "div", "id", "map", "class", "p", "data-paths", track.PathsJSON, "data-points", track.PointsJSON, "data-minzoom", track.MinZoom, "data-maxzoom", track.MaxZoom, "data-attribution", track.MapAttribution, ) - hb.writeElementClose("div") - hb.writeElementOpen("script", "defer", "", "src", a.assetFileName("js/geomap.js")) - hb.writeElementClose("script") + hb.WriteElementClose("div") + hb.WriteElementOpen("script", "defer", "", "src", a.assetFileName("js/geomap.js")) + hb.WriteElementClose("script") } } -func (a *goBlog) renderPostReactions(hb *htmlBuilder, p *post) { +func (a *goBlog) renderPostReactions(hb *htmlbuilder.HtmlBuilder, p *post) { if !a.reactionsEnabledForPost(p) { return } - hb.writeElementOpen("div", "id", "reactions", "class", "actions", "data-path", p.Path, "data-allowed", strings.Join(allowedReactions, ",")) - hb.writeElementClose("div") - hb.writeElementOpen("script", "defer", "", "src", a.assetFileName("js/reactions.js")) - hb.writeElementClose("script") + hb.WriteElementOpen("div", "id", "reactions", "class", "actions", "data-path", p.Path, "data-allowed", strings.Join(allowedReactions, ",")) + hb.WriteElementClose("div") + hb.WriteElementOpen("script", "defer", "", "src", a.assetFileName("js/reactions.js")) + hb.WriteElementClose("script") } -func (a *goBlog) renderPostVideo(hb *htmlBuilder, p *post) { +func (a *goBlog) renderPostVideo(hb *htmlbuilder.HtmlBuilder, p *post) { if !p.hasVideoPlaylist() { return } - hb.writeElementOpen("div", "id", "video", "data-url", p.firstParameter(videoPlaylistParam)) - hb.writeElementClose("div") - hb.writeElementOpen("script", "defer", "", "src", a.assetFileName("js/video.js")) - hb.writeElementClose("script") + hb.WriteElementOpen("div", "id", "video", "data-url", p.firstParameter(videoPlaylistParam)) + hb.WriteElementClose("div") + hb.WriteElementOpen("script", "defer", "", "src", a.assetFileName("js/video.js")) + hb.WriteElementClose("script") } -func (a *goBlog) renderPostSectionSettings(hb *htmlBuilder, rd *renderData, srd *settingsRenderData) { - hb.writeElementOpen("h2") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "postsections")) - hb.writeElementClose("h2") +func (a *goBlog) renderPostSectionSettings(hb *htmlbuilder.HtmlBuilder, rd *renderData, srd *settingsRenderData) { + hb.WriteElementOpen("h2") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "postsections")) + hb.WriteElementClose("h2") // Update default section - hb.writeElementOpen("h3") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "default")) - hb.writeElementClose("h3") + hb.WriteElementOpen("h3") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "default")) + hb.WriteElementClose("h3") - hb.writeElementOpen("form", "class", "fw p", "method", "post") - hb.writeElementOpen("select", "name", "defaultsection") + hb.WriteElementOpen("form", "class", "fw p", "method", "post") + hb.WriteElementOpen("select", "name", "defaultsection") for _, section := range srd.sections { - hb.writeElementOpen("option", "value", section.Name, lo.If(section.Name == srd.defaultSection, "selected").Else(""), "") - hb.writeEscaped(section.Name) - hb.writeElementClose("option") + hb.WriteElementOpen("option", "value", section.Name, lo.If(section.Name == srd.defaultSection, "selected").Else(""), "") + hb.WriteEscaped(section.Name) + hb.WriteElementClose("option") } - hb.writeElementClose("select") - hb.writeElementOpen( + hb.WriteElementClose("select") + hb.WriteElementOpen( "input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update"), "formaction", rd.Blog.getRelativePath(settingsPath+settingsUpdateDefaultSectionPath), ) - hb.writeElementClose("form") + hb.WriteElementClose("form") for _, section := range srd.sections { - hb.writeElementOpen("details") + hb.WriteElementOpen("details") - hb.writeElementOpen("summary") - hb.writeElementOpen("h3") - hb.writeEscaped(section.Name) - hb.writeElementClose("h3") - hb.writeElementClose("summary") + hb.WriteElementOpen("summary") + hb.WriteElementOpen("h3") + hb.WriteEscaped(section.Name) + hb.WriteElementClose("h3") + hb.WriteElementClose("summary") - hb.writeElementOpen("form", "class", "fw p", "method", "post") + hb.WriteElementOpen("form", "class", "fw p", "method", "post") - hb.writeElementOpen("input", "type", "hidden", "name", "sectionname", "value", section.Name) + hb.WriteElementOpen("input", "type", "hidden", "name", "sectionname", "value", section.Name) // Title - hb.writeElementOpen("input", "type", "text", "name", "sectiontitle", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectiontitle"), "required", "", "value", section.Title) + hb.WriteElementOpen("input", "type", "text", "name", "sectiontitle", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectiontitle"), "required", "", "value", section.Title) // Description - hb.writeElementOpen( + hb.WriteElementOpen( "textarea", "name", "sectiondescription", "class", "monospace", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectiondescription"), ) - hb.writeEscaped(section.Description) - hb.writeElementClose("textarea") + hb.WriteEscaped(section.Description) + hb.WriteElementClose("textarea") // Path template - hb.writeElementOpen("input", "type", "text", "name", "sectionpathtemplate", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectionpathtemplate"), "value", section.PathTemplate) + hb.WriteElementOpen("input", "type", "text", "name", "sectionpathtemplate", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectionpathtemplate"), "value", section.PathTemplate) // Show full - hb.writeElementOpen("input", "type", "checkbox", "name", "sectionshowfull", "id", "showfull-"+section.Name, lo.If(section.ShowFull, "checked").Else(""), "") - hb.writeElementOpen("label", "for", "showfull-"+section.Name) - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectionshowfull")) - hb.writeElementClose("label") + hb.WriteElementOpen("input", "type", "checkbox", "name", "sectionshowfull", "id", "showfull-"+section.Name, lo.If(section.ShowFull, "checked").Else(""), "") + hb.WriteElementOpen("label", "for", "showfull-"+section.Name) + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectionshowfull")) + hb.WriteElementClose("label") // Actions - hb.writeElementOpen("div", "class", "p") + hb.WriteElementOpen("div", "class", "p") // Update - hb.writeElementOpen( + hb.WriteElementOpen( "input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update"), "formaction", rd.Blog.getRelativePath(settingsPath+settingsUpdateSectionPath), ) // Delete - hb.writeElementOpen( + hb.WriteElementOpen( "input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete"), "formaction", rd.Blog.getRelativePath(settingsPath+settingsDeleteSectionPath), "class", "confirm", "data-confirmmessage", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "confirmdelete"), ) - hb.writeElementClose("div") + hb.WriteElementClose("div") - hb.writeElementClose("form") - hb.writeElementClose("details") + hb.WriteElementClose("form") + hb.WriteElementClose("details") } // Create new section - hb.writeElementOpen("form", "class", "fw p", "method", "post") + hb.WriteElementOpen("form", "class", "fw p", "method", "post") // Name - hb.writeElementOpen("input", "type", "text", "name", "sectionname", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectionname"), "required", "") + hb.WriteElementOpen("input", "type", "text", "name", "sectionname", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectionname"), "required", "") // Title - hb.writeElementOpen("input", "type", "text", "name", "sectiontitle", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectiontitle"), "required", "") + hb.WriteElementOpen("input", "type", "text", "name", "sectiontitle", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectiontitle"), "required", "") // Create button - hb.writeElementOpen("div") - hb.writeElementOpen( + hb.WriteElementOpen("div") + hb.WriteElementOpen( "input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "create"), "formaction", rd.Blog.getRelativePath(settingsPath+settingsCreateSectionPath), ) - hb.writeElementClose("div") - hb.writeElementClose("form") + hb.WriteElementClose("div") + hb.WriteElementClose("form") } -func (a *goBlog) renderCollapsibleBooleanSetting(hb *htmlBuilder, rd *renderData, path, title, description, name string, value bool) { - hb.writeElementOpen("details") +func (a *goBlog) renderCollapsibleBooleanSetting(hb *htmlbuilder.HtmlBuilder, rd *renderData, path, title, description, name string, value bool) { + hb.WriteElementOpen("details") - hb.writeElementOpen("summary") - hb.writeElementOpen("h3") - hb.writeEscaped(title) - hb.writeElementClose("h3") - hb.writeElementClose("summary") + hb.WriteElementOpen("summary") + hb.WriteElementOpen("h3") + hb.WriteEscaped(title) + hb.WriteElementClose("h3") + hb.WriteElementClose("summary") - hb.writeElementOpen("form", "class", "fw p", "method", "post") + hb.WriteElementOpen("form", "class", "fw p", "method", "post") - hb.writeElementOpen("input", "type", "checkbox", "name", name, "id", "cb-"+name, lo.If(value, "checked").Else(""), "") - hb.writeElementOpen("label", "for", "cb-"+name) - hb.writeEscaped(description) - hb.writeElementClose("label") + hb.WriteElementOpen("input", "type", "checkbox", "name", name, "id", "cb-"+name, lo.If(value, "checked").Else(""), "") + hb.WriteElementOpen("label", "for", "cb-"+name) + hb.WriteEscaped(description) + hb.WriteElementClose("label") - hb.writeElementOpen("div", "class", "p") - hb.writeElementOpen( + hb.WriteElementOpen("div", "class", "p") + hb.WriteElementOpen( "input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update"), "formaction", path, ) - hb.writeElementClose("div") + hb.WriteElementClose("div") - hb.writeElementClose("form") + hb.WriteElementClose("form") - hb.writeElementClose("details") + hb.WriteElementClose("details") } diff --git a/uiHtmlBuilder.go b/uiHtmlBuilder.go deleted file mode 100644 index cca258d..0000000 --- a/uiHtmlBuilder.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "io" - textTemplate "text/template" -) - -type htmlBuilder struct { - w io.Writer -} - -func newHtmlBuilder(w io.Writer) *htmlBuilder { - return &htmlBuilder{ - w: w, - } -} - -func (h *htmlBuilder) getWriter() io.Writer { - return h.w -} - -func (h *htmlBuilder) Write(p []byte) (int, error) { - return h.getWriter().Write(p) -} - -func (h *htmlBuilder) WriteString(s string) (int, error) { - return io.WriteString(h.getWriter(), s) -} - -func (h *htmlBuilder) write(s string) { - _, _ = h.WriteString(s) -} - -func (h *htmlBuilder) writeEscaped(s string) { - textTemplate.HTMLEscape(h, []byte(s)) -} - -func (h *htmlBuilder) writeAttribute(attr string, val any) { - h.write(` `) - h.write(attr) - h.write(`=`) - if valStr, ok := val.(string); ok { - h.write(`"`) - h.writeEscaped(valStr) - h.write(`"`) - } else { - h.writeEscaped(fmt.Sprint(val)) - } -} - -func (h *htmlBuilder) writeElementOpen(tag string, attrs ...any) { - h.write(`<`) - h.write(tag) - for i := 0; i < len(attrs); i += 2 { - if i+2 > len(attrs) { - break - } - attrStr, ok := attrs[i].(string) - if !ok { - continue - } - h.writeAttribute(attrStr, attrs[i+1]) - } - h.write(`>`) -} - -func (h *htmlBuilder) writeElementClose(tag string) { - h.write(``) -} diff --git a/ui_test.go b/ui_test.go index 9ad10c9..95217f5 100644 --- a/ui_test.go +++ b/ui_test.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "io" "os" "strings" "testing" @@ -11,11 +10,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.goblog.app/app/pkgs/bufferpool" + "go.goblog.app/app/pkgs/htmlbuilder" ) -var _ io.Writer = &htmlBuilder{} -var _ io.StringWriter = &htmlBuilder{} - func Test_renderPostTax(t *testing.T) { app := &goBlog{ cfg: createDefaultTestConfig(t), @@ -33,7 +30,7 @@ func Test_renderPostTax(t *testing.T) { buf := bufferpool.Get() defer bufferpool.Put(buf) - hb := newHtmlBuilder(buf) + hb := htmlbuilder.NewHtmlBuilder(buf) app.renderPostTax(hb, p, app.cfg.Blogs["default"]) @@ -56,7 +53,7 @@ func Test_renderOldContentWarning(t *testing.T) { } buf := &bytes.Buffer{} - hb := newHtmlBuilder(buf) + hb := htmlbuilder.NewHtmlBuilder(buf) app.renderOldContentWarning(hb, p, app.cfg.Blogs["default"]) res := buf.String() @@ -122,7 +119,7 @@ func Test_renderInteractions(t *testing.T) { require.NoError(t, err) buf := &bytes.Buffer{} - hb := newHtmlBuilder(buf) + hb := htmlbuilder.NewHtmlBuilder(buf) app.renderInteractions(hb, &renderData{ Blog: app.cfg.Blogs["default"], @@ -149,7 +146,7 @@ func Test_renderAuthor(t *testing.T) { _ = app.initConfig(false) buf := &bytes.Buffer{} - hb := newHtmlBuilder(buf) + hb := htmlbuilder.NewHtmlBuilder(buf) app.renderAuthor(hb) res := buf.String()