mirror of https://github.com/jlelse/GoBlog
parent
2158b156c5
commit
a45d28d04f
|
@ -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)
|
||||
- [GoBlog's storage system](./storage.md)
|
||||
- [GoBlog Plugins](./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.
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
2
go.mod
2
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
2
http.go
2
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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(`</`)
|
||||
h.WriteUnescaped(tag)
|
||||
h.WriteUnescaped(`>`)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package htmlbuilder
|
||||
|
||||
import "io"
|
||||
|
||||
var _ io.Writer = &HtmlBuilder{}
|
||||
var _ io.StringWriter = &HtmlBuilder{}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)),
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
36
plugins.go
36
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}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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))
|
||||
|
|
609
uiComponents.go
609
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)
|
||||