Rework plugins (Part 1)

This commit is contained in:
Jan-Lukas Else 2023-01-22 21:26:21 +01:00
parent bff6272350
commit 567eeb1116
25 changed files with 632 additions and 693 deletions

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 - 2022 Jan-Lukas Else
Copyright (c) 2020 - 2023 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

View File

@ -350,7 +350,6 @@ type configPprof struct {
type configPlugin struct {
Path string `mapstructure:"path"`
Type string `mapstructure:"type"`
Import string `mapstructure:"import"`
Config map[string]any `mapstructure:"config"`
}

15
go.mod
View File

@ -36,7 +36,7 @@ require (
github.com/jlelse/feeds v1.2.1-0.20210704161900-189f94254ad4
github.com/justinas/alice v1.2.0
github.com/kaorimatz/go-opml v0.0.0-20210201121027-bc8e2852d7f9
github.com/klauspost/compress v1.15.14
github.com/klauspost/compress v1.15.15
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/lopezator/migrator v0.3.1
github.com/mattn/go-sqlite3 v1.14.16
@ -50,7 +50,7 @@ require (
github.com/schollz/sqlite3dump v1.3.1
github.com/snabb/sitemap v1.0.0
github.com/spf13/cast v1.5.0
github.com/spf13/viper v1.14.0
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.1
github.com/tdewolff/minify/v2 v2.12.4
// master
@ -67,7 +67,7 @@ require (
golang.org/x/sync v0.1.0
golang.org/x/text v0.6.0
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.12.4
maunium.net/go/mautrix v0.13.0
nhooyr.io/websocket v1.8.7
// main
willnorris.com/go/microformats v1.1.2-0.20221115043057-ffbbdaef989e
@ -98,24 +98,23 @@ require (
github.com/jonboulle/clockwork v0.3.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/lestrrat-go/strftime v1.0.6 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/xid v1.4.0 // indirect
github.com/rs/zerolog v1.28.0 // indirect
github.com/snabb/diagio v1.0.0 // indirect
github.com/spf13/afero v1.9.2 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/tdewolff/parse/v2 v2.6.4 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect

30
go.sum
View File

@ -278,8 +278,8 @@ github.com/kaorimatz/go-opml v0.0.0-20210201121027-bc8e2852d7f9 h1:+9REu9CK9D1AQ
github.com/kaorimatz/go-opml v0.0.0-20210201121027-bc8e2852d7f9/go.mod h1:OvY5ZBrAC9kOvM2PZs9Lw0BH+5K7tjrT6T7SFhn27OA=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.15.14 h1:i7WCKDToww0wA+9qrUZ1xOjp218vfFo3nTU6UHp+gOc=
github.com/klauspost/compress v1.15.14/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@ -299,8 +299,8 @@ github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205Ah
github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw=
github.com/lopezator/migrator v0.3.1 h1:ZFPT6aC7+nGWkqhleynABZ6ftycSf6hmHHLOaryq1Og=
github.com/lopezator/migrator v0.3.1/go.mod h1:X+lHDMZ9Ci3/KdbypJcQYFFwipVrJsX4fRCQ4QLauYk=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -332,10 +332,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1DoCgY=
github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
@ -364,16 +362,16 @@ github.com/snabb/diagio v1.0.0 h1:kovhQ1rDXoEbmpf/T5N2sUp2iOdxEg+TcqzbYVHV2V0=
github.com/snabb/diagio v1.0.0/go.mod h1:ZyGaWFhfBVqstGUw6laYetzeTwZ2xxVPqTALx1QQa1w=
github.com/snabb/sitemap v1.0.0 h1:7vJeNPAaaj7fQSRS3WYuJHzUjdnhLdSLLpvVtnhbzC0=
github.com/snabb/sitemap v1.0.0/go.mod h1:Id8uz1+WYdiNmSjEi4BIvL5UwNPYLsTHzRbjmDwNDzA=
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU=
github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -386,8 +384,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tdewolff/minify/v2 v2.12.4 h1:kejsHQMM17n6/gwdw53qsi6lg0TGddZADVyQOz1KMdE=
github.com/tdewolff/minify/v2 v2.12.4/go.mod h1:h+SRvSIX3kwgwTFOpSckvSxgax3uy8kZTSF1Ojrr3bk=
github.com/tdewolff/parse/v2 v2.6.4 h1:KCkDvNUMof10e3QExio9OPZJT8SbdKojLBumw8YZycQ=
@ -773,8 +771,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
maunium.net/go/mautrix v0.12.4 h1:TAg+qkgZLlD2wvshFEuGCv7kADnAbQ6NZmTPu7wsXZI=
maunium.net/go/mautrix v0.12.4/go.mod h1:NBN7/dch8xMnt4VEV9nucVOkzbP4PHr3agXJrFpM5AE=
maunium.net/go/mautrix v0.13.0 h1:CRdpMFc1kDSNnCZMcqahR9/pkDy/vgRbd+fHnSCl6Yg=
maunium.net/go/mautrix v0.13.0/go.mod h1:gYMQPsZ9lQpyKlVp+DGwOuc9LIcE/c8GZW2CvKHISgM=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

View File

@ -16,6 +16,7 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/justinas/alice"
"github.com/klauspost/compress/flate"
"github.com/samber/lo"
"go.goblog.app/app/pkgs/httpcompress"
"go.goblog.app/app/pkgs/maprouter"
"go.goblog.app/app/pkgs/plugintypes"
@ -46,7 +47,7 @@ func (a *goBlog) startServer() (err error) {
h = h.Append(a.securityHeaders)
}
// Add plugin middlewares
middlewarePlugins := getPluginsForType[plugintypes.Middleware](a, middlewarePlugin)
middlewarePlugins := lo.Map(a.getPlugins(pluginMiddlewareType), func(item any, index int) plugintypes.Middleware { return item.(plugintypes.Middleware) })
sort.Slice(middlewarePlugins, func(i, j int) bool {
// Sort with descending prio
return middlewarePlugins[i].Prio() > middlewarePlugins[j].Prio()

View File

@ -1,59 +0,0 @@
package plugins
import (
"fmt"
"path/filepath"
"reflect"
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
)
type plugin struct {
Config *PluginConfig
plugin reflect.Value
}
// PluginConfig is the configuration of the plugin.
type PluginConfig struct {
// Path is the storage path of the plugin.
Path string
// ImportPath is the module path i.e. "github.com/user/module".
ImportPath string
// PluginType is the type of plugin, this plugin is checked against that type.
// The available types are specified by the implementor of this package.
PluginType string
}
func (p *plugin) initPlugin(host *PluginHost) error {
const errText = "initPlugin: %w"
interpreter := interp.New(interp.Options{
GoPath: p.Config.Path,
})
if err := interpreter.Use(stdlib.Symbols); err != nil {
return fmt.Errorf(errText, err)
}
if err := interpreter.Use(host.Symbols); err != nil {
return fmt.Errorf(errText, err)
}
if _, err := interpreter.Eval(fmt.Sprintf(`import "%s"`, p.Config.ImportPath)); err != nil {
return fmt.Errorf(errText, err)
}
v, err := interpreter.Eval(filepath.Base(p.Config.ImportPath) + ".GetPlugin")
if err != nil {
return fmt.Errorf(errText, err)
}
result := v.Call([]reflect.Value{})
if len(result) > 1 {
return fmt.Errorf(errText+": function GetPlugin has more than one return value", ErrValidatingPlugin)
}
p.plugin = result[0]
return nil
}

View File

@ -1,80 +1,124 @@
package plugins
import (
"errors"
"fmt"
"io/fs"
"path/filepath"
"reflect"
"strings"
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
)
// PluginHost manages the plugins.
type PluginHost struct {
plugins map[string][]*plugin
pluginTypes map[string]reflect.Type
symbols interp.Exports
embeddedPlugins fs.FS
}
// PluginConfig is the configuration of the plugin.
type PluginConfig struct {
// Path is the storage path of the plugin.
Path string
// ImportPath is the module path i.e. "github.com/user/module".
ImportPath string
}
type plugin struct {
Config *PluginConfig
plugin any
}
var (
// ErrValidatingPlugin is returned when the plugin fails to fully implement the interface of the plugin type.
ErrValidatingPlugin = errors.New("plugin does not implement type")
// ErrInitFunc is returned when the plugin has a faulty initialization function.
ErrInitFunc = errors.New("bad plugin init function")
)
const (
embeddedPrefix = "embedded:"
)
// NewPluginHost initializes a PluginHost.
func NewPluginHost(symbols interp.Exports) *PluginHost {
func NewPluginHost(pluginTypes map[string]reflect.Type, symbols interp.Exports, embeddedPlugins fs.FS) *PluginHost {
return &PluginHost{
Plugins: []*plugin{},
PluginTypes: map[string]reflect.Type{},
Symbols: symbols,
plugins: map[string][]*plugin{},
pluginTypes: pluginTypes,
symbols: symbols,
embeddedPlugins: embeddedPlugins,
}
}
// AddPluginType adds a plugin type to the list.
// The interface for the pluginType parameter should be a nil of the plugin type interface:
//
// (*PluginInterface)(nil)
func (h *PluginHost) AddPluginType(name string, pluginType interface{}) {
h.PluginTypes[name] = reflect.TypeOf(pluginType).Elem()
}
// LoadPlugin loads a new plugin to the host.
func (h *PluginHost) LoadPlugin(config *PluginConfig) (any, error) {
func (h *PluginHost) LoadPlugin(config *PluginConfig) (map[string]any, error) {
p := &plugin{
Config: config,
}
err := p.initPlugin(h)
plugins, err := p.initPlugin(h)
if err != nil {
return nil, err
}
err = h.validatePlugin(p)
if err != nil {
return nil, err
}
h.Plugins = append(h.Plugins, p)
return p.plugin.Interface(), nil
}
func (h *PluginHost) validatePlugin(p *plugin) error {
pType := reflect.TypeOf(p.plugin.Interface())
if _, ok := h.PluginTypes[p.Config.PluginType]; !ok {
return fmt.Errorf("validatePlugin: %v: %w", p.Config.PluginType, ErrInvalidType)
}
if !pType.Implements(h.PluginTypes[p.Config.PluginType]) {
return fmt.Errorf("validatePlugin:%v: %w %v", p, ErrValidatingPlugin, p.Config.PluginType)
}
return nil
return plugins, nil
}
// GetPlugins returns a list of all plugins.
func (h *PluginHost) GetPlugins() (list []any) {
for _, p := range h.Plugins {
list = append(list, p.plugin.Interface())
func (h *PluginHost) GetPlugins(typ string) (list []any) {
for _, p := range h.plugins[typ] {
list = append(list, p.plugin)
}
return
}
// GetPluginsForType returns all the plugins that are of type pluginType or empty if the pluginType doesn't exist.
func GetPluginsForType[T any](h *PluginHost, pluginType string) (list []T) {
if _, ok := h.PluginTypes[pluginType]; !ok {
return
func (p *plugin) initPlugin(host *PluginHost) (plugins map[string]any, err error) {
const errText = "initPlugin: %w"
plugins = map[string]any{}
var filesystem fs.FS
if strings.HasPrefix(p.Config.Path, embeddedPrefix) {
filesystem = host.embeddedPlugins
}
for _, p := range h.Plugins {
if p.Config.PluginType != pluginType {
continue
interpreter := interp.New(interp.Options{
GoPath: strings.TrimPrefix(p.Config.Path, embeddedPrefix),
SourcecodeFilesystem: filesystem,
})
if err := interpreter.Use(stdlib.Symbols); err != nil {
return nil, fmt.Errorf(errText, err)
}
if err := interpreter.Use(host.symbols); err != nil {
return nil, fmt.Errorf(errText, err)
}
if _, err := interpreter.Eval(fmt.Sprintf(`import "%s"`, p.Config.ImportPath)); err != nil {
return nil, fmt.Errorf(errText, err)
}
v, err := interpreter.Eval(filepath.Base(p.Config.ImportPath) + ".GetPlugin")
if err != nil {
return nil, fmt.Errorf(errText, err)
}
resultArray := v.Call([]reflect.Value{})
for _, result := range resultArray {
newPlugin := &plugin{
Config: p.Config,
plugin: result.Interface(),
}
if t, ok := p.plugin.Interface().(T); ok {
list = append(list, t)
for name, reflectType := range host.pluginTypes {
if result.Type().Implements(reflectType) {
host.plugins[name] = append(host.plugins[name], newPlugin)
plugins[name] = newPlugin.plugin
}
}
}
return
}

View File

@ -1,25 +0,0 @@
package plugins
import (
"errors"
"reflect"
"github.com/traefik/yaegi/interp"
)
// PluginHost manages the plugins.
type PluginHost struct {
// Plugins contains a list of the plugins.
Plugins []*plugin
// PluginTypes is a list of plugins types that plugins have to use at least one of.
PluginTypes map[string]reflect.Type
// Symbols is the map of symbols generated by yaegi extract.
Symbols interp.Exports
}
var (
// ErrInvalidType is returned when the plugin type specified by the plugin is invalid.
ErrInvalidType = errors.New("invalid plugin type")
// ErrValidatingPlugin is returned when the plugin fails to fully implement the interface of the plugin type.
ErrValidatingPlugin = errors.New("plugin does not implement type")
)

View File

@ -3,13 +3,12 @@ package plugintypes
import (
"context"
"database/sql"
"go.goblog.app/app/pkgs/htmlbuilder"
)
// App is used to access GoBlog's app instance.
type App interface {
GetDatabase() Database
GetPost(path string) (Post, error)
}
// Database is used to provide access to GoBlog's database.
@ -27,36 +26,8 @@ type Post interface {
GetParameters() map[string][]string
}
// Blog
type Blog interface {
// RenderContext
type RenderContext interface {
GetPath() string
GetBlog() string
}
// RenderType
type RenderType string
// RenderData
type RenderData interface {
// Empty
}
// RenderNextFunc
type RenderNextFunc func(*htmlbuilder.HtmlBuilder)
// 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
}
// Render footer element on every blog page, data = BlogRenderData
const BlogFooterRenderType RenderType = "blog-footer"
// BlogRenderData is RenderData containing a Blog
type BlogRenderData interface {
RenderData
GetBlog() Blog
}

View File

@ -1,38 +1,29 @@
package plugintypes
import (
"io"
"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.
// SetApp is used to allow GoBlog set its app instance to be accessible by the plugin.
type SetApp interface {
SetApp(App)
SetApp(app App)
}
// SetConfig is used in all plugin types to allow
// GoBlog set plugin configuration.
// SetConfig is used in all plugin types to allow GoBlog set the plugin configuration.
type SetConfig interface {
SetConfig(map[string]any)
SetConfig(config map[string]any)
}
type Exec interface {
SetApp
SetConfig
Exec()
}
type Middleware interface {
SetApp
SetConfig
Handler(http.Handler) http.Handler
Handler(next http.Handler) http.Handler
Prio() int
}
type UI interface {
SetApp
SetConfig
Render(*htmlbuilder.HtmlBuilder, RenderType, RenderData, RenderNextFunc)
Render(renderContext RenderContext, rendered io.Reader, modified io.Writer)
}

View File

@ -0,0 +1,74 @@
// Code generated by 'yaegi extract github.com/PuerkitoBio/goquery'. DO NOT EDIT.
// MIT License
//
// Copyright (c) 2020 - 2023 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 (
"github.com/PuerkitoBio/goquery"
"golang.org/x/net/html"
"reflect"
)
func init() {
Symbols["github.com/PuerkitoBio/goquery/goquery"] = map[string]reflect.Value{
// function, constant and variable definitions
"CloneDocument": reflect.ValueOf(goquery.CloneDocument),
"NewDocument": reflect.ValueOf(goquery.NewDocument),
"NewDocumentFromNode": reflect.ValueOf(goquery.NewDocumentFromNode),
"NewDocumentFromReader": reflect.ValueOf(goquery.NewDocumentFromReader),
"NewDocumentFromResponse": reflect.ValueOf(goquery.NewDocumentFromResponse),
"NodeName": reflect.ValueOf(goquery.NodeName),
"OuterHtml": reflect.ValueOf(goquery.OuterHtml),
"Render": reflect.ValueOf(goquery.Render),
"Single": reflect.ValueOf(goquery.Single),
"SingleMatcher": reflect.ValueOf(goquery.SingleMatcher),
"ToEnd": reflect.ValueOf(goquery.ToEnd),
// type definitions
"Document": reflect.ValueOf((*goquery.Document)(nil)),
"Matcher": reflect.ValueOf((*goquery.Matcher)(nil)),
"Selection": reflect.ValueOf((*goquery.Selection)(nil)),
// interface wrapper definitions
"_Matcher": reflect.ValueOf((*_github_com_PuerkitoBio_goquery_Matcher)(nil)),
}
}
// _github_com_PuerkitoBio_goquery_Matcher is an interface wrapper for Matcher type
type _github_com_PuerkitoBio_goquery_Matcher struct {
IValue interface{}
WFilter func(a0 []*html.Node) []*html.Node
WMatch func(a0 *html.Node) bool
WMatchAll func(a0 *html.Node) []*html.Node
}
func (W _github_com_PuerkitoBio_goquery_Matcher) Filter(a0 []*html.Node) []*html.Node {
return W.WFilter(a0)
}
func (W _github_com_PuerkitoBio_goquery_Matcher) Match(a0 *html.Node) bool {
return W.WMatch(a0)
}
func (W _github_com_PuerkitoBio_goquery_Matcher) MatchAll(a0 *html.Node) []*html.Node {
return W.WMatchAll(a0)
}

View File

@ -0,0 +1,38 @@
// Code generated by 'yaegi extract go.goblog.app/app/pkgs/bufferpool'. DO NOT EDIT.
// MIT License
//
// Copyright (c) 2020 - 2023 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/bufferpool"
"reflect"
)
func init() {
Symbols["go.goblog.app/app/pkgs/bufferpool/bufferpool"] = map[string]reflect.Value{
// function, constant and variable definitions
"Get": reflect.ValueOf(bufferpool.Get),
"Put": reflect.ValueOf(bufferpool.Put),
}
}

View File

@ -2,7 +2,7 @@
// MIT License
//
// Copyright (c) 2020 - 2022 Jan-Lukas Else
// Copyright (c) 2020 - 2023 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

View File

@ -2,7 +2,7 @@
// MIT License
//
// Copyright (c) 2020 - 2022 Jan-Lukas Else
// Copyright (c) 2020 - 2023 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
@ -27,47 +27,35 @@ package yaegiwrappers
import (
"context"
"database/sql"
"go.goblog.app/app/pkgs/htmlbuilder"
"go.goblog.app/app/pkgs/plugintypes"
"io"
"net/http"
"reflect"
)
func init() {
Symbols["go.goblog.app/app/pkgs/plugintypes/plugintypes"] = map[string]reflect.Value{
// function, constant and variable definitions
"BlogFooterRenderType": reflect.ValueOf(plugintypes.BlogFooterRenderType),
"PostMainElementRenderType": reflect.ValueOf(plugintypes.PostMainElementRenderType),
// type definitions
"App": reflect.ValueOf((*plugintypes.App)(nil)),
"Blog": reflect.ValueOf((*plugintypes.Blog)(nil)),
"BlogRenderData": reflect.ValueOf((*plugintypes.BlogRenderData)(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)),
"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)),
"RenderContext": reflect.ValueOf((*plugintypes.RenderContext)(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)),
"_Blog": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Blog)(nil)),
"_BlogRenderData": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_BlogRenderData)(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)),
"_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)),
"_RenderContext": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_RenderContext)(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)),
}
}
@ -75,30 +63,14 @@ func init() {
type _go_goblog_app_app_pkgs_plugintypes_App struct {
IValue interface{}
WGetDatabase func() plugintypes.Database
WGetPost func(path string) (plugintypes.Post, error)
}
func (W _go_goblog_app_app_pkgs_plugintypes_App) GetDatabase() plugintypes.Database {
return W.WGetDatabase()
}
// _go_goblog_app_app_pkgs_plugintypes_Blog is an interface wrapper for Blog type
type _go_goblog_app_app_pkgs_plugintypes_Blog struct {
IValue interface{}
WGetBlog func() string
}
func (W _go_goblog_app_app_pkgs_plugintypes_Blog) GetBlog() string {
return W.WGetBlog()
}
// _go_goblog_app_app_pkgs_plugintypes_BlogRenderData is an interface wrapper for BlogRenderData type
type _go_goblog_app_app_pkgs_plugintypes_BlogRenderData struct {
IValue interface{}
WGetBlog func() plugintypes.Blog
}
func (W _go_goblog_app_app_pkgs_plugintypes_BlogRenderData) GetBlog() plugintypes.Blog {
return W.WGetBlog()
func (W _go_goblog_app_app_pkgs_plugintypes_App) GetPost(path string) (plugintypes.Post, error) {
return W.WGetPost(path)
}
// _go_goblog_app_app_pkgs_plugintypes_Database is an interface wrapper for Database type
@ -133,43 +105,27 @@ func (W _go_goblog_app_app_pkgs_plugintypes_Database) QueryRowContext(a0 context
// _go_goblog_app_app_pkgs_plugintypes_Exec is an interface wrapper for Exec type
type _go_goblog_app_app_pkgs_plugintypes_Exec struct {
IValue interface{}
WExec func()
WSetApp func(a0 plugintypes.App)
WSetConfig func(a0 map[string]any)
IValue interface{}
WExec func()
}
func (W _go_goblog_app_app_pkgs_plugintypes_Exec) Exec() {
W.WExec()
}
func (W _go_goblog_app_app_pkgs_plugintypes_Exec) SetApp(a0 plugintypes.App) {
W.WSetApp(a0)
}
func (W _go_goblog_app_app_pkgs_plugintypes_Exec) SetConfig(a0 map[string]any) {
W.WSetConfig(a0)
}
// _go_goblog_app_app_pkgs_plugintypes_Middleware is an interface wrapper for Middleware type
type _go_goblog_app_app_pkgs_plugintypes_Middleware struct {
IValue interface{}
WHandler func(a0 http.Handler) http.Handler
WPrio func() int
WSetApp func(a0 plugintypes.App)
WSetConfig func(a0 map[string]any)
IValue interface{}
WHandler func(next http.Handler) http.Handler
WPrio func() int
}
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) Handler(a0 http.Handler) http.Handler {
return W.WHandler(a0)
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) Handler(next http.Handler) http.Handler {
return W.WHandler(next)
}
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) Prio() int {
return W.WPrio()
}
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) SetApp(a0 plugintypes.App) {
W.WSetApp(a0)
}
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) SetConfig(a0 map[string]any) {
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 {
@ -181,55 +137,46 @@ func (W _go_goblog_app_app_pkgs_plugintypes_Post) GetParameters() map[string][]s
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 {
// _go_goblog_app_app_pkgs_plugintypes_RenderContext is an interface wrapper for RenderContext type
type _go_goblog_app_app_pkgs_plugintypes_RenderContext struct {
IValue interface{}
WGetPost func() plugintypes.Post
WGetBlog func() string
WGetPath func() string
}
func (W _go_goblog_app_app_pkgs_plugintypes_PostRenderData) GetPost() plugintypes.Post {
return W.WGetPost()
func (W _go_goblog_app_app_pkgs_plugintypes_RenderContext) GetBlog() string {
return W.WGetBlog()
}
// _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{}
func (W _go_goblog_app_app_pkgs_plugintypes_RenderContext) GetPath() string {
return W.WGetPath()
}
// _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{}
WSetApp func(a0 plugintypes.App)
WSetApp func(app plugintypes.App)
}
func (W _go_goblog_app_app_pkgs_plugintypes_SetApp) SetApp(a0 plugintypes.App) {
W.WSetApp(a0)
func (W _go_goblog_app_app_pkgs_plugintypes_SetApp) SetApp(app plugintypes.App) {
W.WSetApp(app)
}
// _go_goblog_app_app_pkgs_plugintypes_SetConfig is an interface wrapper for SetConfig type
type _go_goblog_app_app_pkgs_plugintypes_SetConfig struct {
IValue interface{}
WSetConfig func(a0 map[string]any)
WSetConfig func(config map[string]any)
}
func (W _go_goblog_app_app_pkgs_plugintypes_SetConfig) SetConfig(a0 map[string]any) {
W.WSetConfig(a0)
func (W _go_goblog_app_app_pkgs_plugintypes_SetConfig) SetConfig(config map[string]any) {
W.WSetConfig(config)
}
// _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)
IValue interface{}
WRender func(renderContext plugintypes.RenderContext, rendered io.Reader, modified io.Writer)
}
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)
func (W _go_goblog_app_app_pkgs_plugintypes_UI) Render(renderContext plugintypes.RenderContext, rendered io.Reader, modified io.Writer) {
W.WRender(renderContext, rendered, modified)
}

View File

@ -8,5 +8,10 @@ var (
Symbols = make(map[string]map[string]reflect.Value)
)
// GoBlog packages
//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
//go:generate yaegi extract -license ../../LICENSE -name yaegiwrappers go.goblog.app/app/pkgs/bufferpool
// Dependencies
//go:generate yaegi extract -license ../../LICENSE -name yaegiwrappers github.com/PuerkitoBio/goquery

View File

@ -1,54 +1,68 @@
package main
import (
"embed"
"io/fs"
"reflect"
"go.goblog.app/app/pkgs/plugins"
"go.goblog.app/app/pkgs/plugintypes"
"go.goblog.app/app/pkgs/yaegiwrappers"
)
//go:embed plugins/*
var pluginsFS embed.FS
const (
execPlugin = "exec"
middlewarePlugin = "middleware"
uiPlugin = "ui"
pluginSetAppType = "setapp"
pluginSetConfigType = "setconfig"
pluginUiType = "ui"
pluginExecType = "exec"
pluginMiddlewareType = "middleware"
)
func (a *goBlog) initPlugins() error {
a.pluginHost = plugins.NewPluginHost(yaegiwrappers.Symbols)
a.pluginHost.AddPluginType(execPlugin, (*plugintypes.Exec)(nil))
a.pluginHost.AddPluginType(middlewarePlugin, (*plugintypes.Middleware)(nil))
a.pluginHost.AddPluginType(uiPlugin, (*plugintypes.UI)(nil))
subFS, err := fs.Sub(pluginsFS, "plugins")
if err != nil {
return err
}
a.pluginHost = plugins.NewPluginHost(
map[string]reflect.Type{
pluginSetAppType: reflect.TypeOf((*plugintypes.SetApp)(nil)).Elem(),
pluginSetConfigType: reflect.TypeOf((*plugintypes.SetConfig)(nil)).Elem(),
pluginUiType: reflect.TypeOf((*plugintypes.UI)(nil)).Elem(),
pluginExecType: reflect.TypeOf((*plugintypes.Exec)(nil)).Elem(),
pluginMiddlewareType: reflect.TypeOf((*plugintypes.Middleware)(nil)).Elem(),
},
yaegiwrappers.Symbols,
subFS,
)
for _, pc := range a.cfg.Plugins {
if pluginInterface, err := a.pluginHost.LoadPlugin(&plugins.PluginConfig{
plugins, err := a.pluginHost.LoadPlugin(&plugins.PluginConfig{
Path: pc.Path,
ImportPath: pc.Import,
PluginType: pc.Type,
}); err != nil {
})
if err != nil {
return err
} else if pluginInterface != nil {
if setAppPlugin, ok := pluginInterface.(plugintypes.SetApp); ok {
setAppPlugin.SetApp(a)
}
if setConfigPlugin, ok := pluginInterface.(plugintypes.SetConfig); ok {
setConfigPlugin.SetConfig(pc.Config)
}
}
if p, ok := plugins[pluginSetConfigType]; ok {
p.(plugintypes.SetConfig).SetConfig(pc.Config)
}
if p, ok := plugins[pluginSetAppType]; ok {
p.(plugintypes.SetApp).SetApp(a)
}
}
execs := getPluginsForType[plugintypes.Exec](a, execPlugin)
for _, p := range execs {
go p.Exec()
for _, p := range a.getPlugins(pluginExecType) {
go p.(plugintypes.Exec).Exec()
}
return nil
}
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)
func (a *goBlog) getPlugins(typ string) []any {
return a.pluginHost.GetPlugins(typ)
}
// Implement all needed interfaces
@ -57,34 +71,10 @@ func (a *goBlog) GetDatabase() plugintypes.Database {
return a.db
}
func (a *goBlog) GetPost(path string) (plugintypes.Post, error) {
return a.getPost(path)
}
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}
}
func (b *configBlog) GetBlog() string {
return b.name
}
type pluginBlogRenderData struct {
b *configBlog
}
func (d *pluginBlogRenderData) GetBlog() plugintypes.Blog {
return d.b
}
func (b *configBlog) pluginRenderData() plugintypes.BlogRenderData {
return &pluginBlogRenderData{b: b}
}

View File

@ -0,0 +1,87 @@
package demo
import (
"fmt"
"io"
"net/http"
"github.com/PuerkitoBio/goquery"
"go.goblog.app/app/pkgs/bufferpool"
"go.goblog.app/app/pkgs/htmlbuilder"
"go.goblog.app/app/pkgs/plugintypes"
)
type plugin struct {
app plugintypes.App
config map[string]any
}
func GetPlugin() (
plugintypes.SetApp, plugintypes.SetConfig,
plugintypes.UI,
plugintypes.Exec,
plugintypes.Middleware,
) {
p := &plugin{}
return p, p, p, p, p
}
// SetApp
func (p *plugin) SetApp(app plugintypes.App) {
p.app = app
}
// SetConfig
func (p *plugin) SetConfig(config map[string]any) {
p.config = config
}
// UI
func (p *plugin) Render(_ plugintypes.RenderContext, rendered io.Reader, modified io.Writer) {
doc, err := goquery.NewDocumentFromReader(rendered)
if err != nil {
fmt.Println("demoui plugin: " + err.Error())
return
}
buf := bufferpool.Get()
defer bufferpool.Put(buf)
hb := htmlbuilder.NewHtmlBuilder(buf)
hb.WriteElementOpen("p")
hb.WriteEscaped("End of post content")
hb.WriteElementClose("p")
doc.Find("main.h-entry article div.e-content").AppendHtml(buf.String())
_ = goquery.Render(modified, doc.Selection)
}
// Exec
func (p *plugin) Exec() {
fmt.Println("Hello World from the demo plugin!")
row, _ := p.app.GetDatabase().QueryRow("select count (*) from posts")
var count int
if err := row.Scan(&count); err != nil {
fmt.Println(fmt.Errorf("failed to count posts: %w", err))
return
}
fmt.Printf("Number of posts in database: %d", count)
fmt.Println()
}
// Middleware
func (p *plugin) Prio() int {
if prioAny, ok := p.config["prio"]; ok {
if prio, ok := prioAny.(int); ok {
return prio
}
}
return 100
}
// Middleware
func (p *plugin) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Demo", fmt.Sprintf("This is from the demo middleware with prio %d", p.Prio()))
next.ServeHTTP(w, r)
})
}

View File

@ -1,37 +0,0 @@
package demoexec
import (
"fmt"
"go.goblog.app/app/pkgs/plugintypes"
)
func GetPlugin() plugintypes.Exec {
return &plugin{}
}
type plugin struct {
app plugintypes.App
}
func (p *plugin) SetApp(app plugintypes.App) {
p.app = app
}
func (*plugin) SetConfig(_ map[string]any) {
// Ignore
}
func (p *plugin) Exec() {
fmt.Println("Hello World from the demo plugin!")
row, _ := p.app.GetDatabase().QueryRow("select count (*) from posts")
var count int
if err := row.Scan(&count); err != nil {
fmt.Println(fmt.Errorf("failed to count posts: %w", err))
return
}
fmt.Printf("Number of posts in database: %d", count)
fmt.Println()
}

View File

@ -1,41 +0,0 @@
package demomiddleware
import (
"fmt"
"net/http"
"go.goblog.app/app/pkgs/plugintypes"
)
func GetPlugin() plugintypes.Middleware {
return &plugin{}
}
type plugin struct {
app plugintypes.App
config map[string]any
}
func (p *plugin) SetApp(app plugintypes.App) {
p.app = app
}
func (p *plugin) SetConfig(config map[string]any) {
p.config = config
}
func (p *plugin) Prio() int {
if prioAny, ok := p.config["prio"]; ok {
if prio, ok := prioAny.(int); ok {
return prio
}
}
return 100
}
func (p *plugin) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Demo", fmt.Sprintf("This is from the demo middleware with prio %d", p.Prio()))
next.ServeHTTP(w, r)
})
}

View File

@ -1,36 +0,0 @@
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)
}
}

View File

@ -2,53 +2,64 @@ package syndication
import (
"fmt"
"io"
"github.com/PuerkitoBio/goquery"
"go.goblog.app/app/pkgs/bufferpool"
"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
app plugintypes.App
parameterName string
}
func (*plugin) SetApp(_ plugintypes.App) {
// Ignore
func GetPlugin() (plugintypes.SetConfig, plugintypes.SetApp, plugintypes.UI) {
p := &plugin{}
return p, p, p
}
func (p *plugin) SetApp(app plugintypes.App) {
p.app = app
}
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
p.parameterName = "syndication" // default
if configParameterAny, ok := config["parameter"]; ok {
if configParameter, ok := configParameterAny.(string); ok {
p.parameterName = configParameter // override default from config
}
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)
}
}
func (p *plugin) Render(rc plugintypes.RenderContext, rendered io.Reader, modified io.Writer) {
def := func() {
_, _ = io.Copy(modified, rendered)
}
post, err := p.app.GetPost(rc.GetPath())
if err != nil || post == nil {
def()
return
}
syndicationLinks, ok := post.GetParameters()[p.parameterName]
if !ok || len(syndicationLinks) == 0 {
def()
return
}
doc, err := goquery.NewDocumentFromReader(rendered)
if err != nil {
fmt.Println("syndication plugin: " + err.Error())
def()
return
}
buf := bufferpool.Get()
defer bufferpool.Put(buf)
hb := htmlbuilder.NewHtmlBuilder(buf)
for _, link := range syndicationLinks {
hb.WriteElementOpen("data", "value", link, "class", "u-syndication hide")
hb.WriteElementClose("data")
}
doc.Find("main.h-entry article").AppendHtml(buf.String())
_ = goquery.Render(modified, doc.Selection)
}

View File

@ -2,82 +2,79 @@ package webrings
import (
"fmt"
"io"
"github.com/PuerkitoBio/goquery"
"go.goblog.app/app/pkgs/bufferpool"
"go.goblog.app/app/pkgs/htmlbuilder"
"go.goblog.app/app/pkgs/plugintypes"
)
func GetPlugin() plugintypes.UI {
return &plugin{}
func GetPlugin() (plugintypes.SetConfig, plugintypes.UI) {
p := &plugin{}
return p, p
}
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) {
render(hb)
if t == plugintypes.BlogFooterRenderType {
bd, ok := data.(plugintypes.BlogRenderData)
if !ok {
fmt.Println("webrings plugin: data is not BlogRenderData!")
return
}
blogData := bd.GetBlog()
if blogData == nil {
fmt.Println("webrings plugin: blog is nil!")
return
}
blog := blogData.GetBlog()
if blog == "" {
fmt.Println("webrings plugin: blog is empty!")
return
}
if blogWebringsAny, ok := p.config[blog]; ok {
if blogWebrings, ok := blogWebringsAny.([]any); ok {
for _, webringAny := range blogWebrings {
if webring, ok := webringAny.(map[string]any); ok {
title, titleOk := unwrapToString(webring["title"])
link, linkOk := unwrapToString(webring["link"])
prev, prevOk := unwrapToString(webring["prev"])
next, nextOk := unwrapToString(webring["next"])
if titleOk && (linkOk || prevOk || nextOk) {
hb.WriteElementOpen("p")
if prevOk {
hb.WriteElementOpen("a", "href", prev)
hb.WriteEscaped("←")
hb.WriteElementClose("a")
hb.WriteEscaped(" ")
}
if linkOk {
hb.WriteElementOpen("a", "href", link)
}
hb.WriteEscaped(title)
if linkOk {
hb.WriteElementClose("a")
}
if nextOk {
hb.WriteEscaped(" ")
hb.WriteElementOpen("a", "href", next)
hb.WriteEscaped("→")
hb.WriteElementClose("a")
}
hb.WriteElementClose("p")
func (p *plugin) Render(rc plugintypes.RenderContext, rendered io.Reader, modified io.Writer) {
blog := rc.GetBlog()
if blog == "" {
fmt.Println("webrings plugin: blog is empty!")
return
}
doc, err := goquery.NewDocumentFromReader(rendered)
if err != nil {
fmt.Println("webrings plugin: " + err.Error())
return
}
if blogWebringsAny, ok := p.config[blog]; ok {
if blogWebrings, ok := blogWebringsAny.([]any); ok {
buf := bufferpool.Get()
defer bufferpool.Put(buf)
hb := htmlbuilder.NewHtmlBuilder(buf)
for _, webringAny := range blogWebrings {
if webring, ok := webringAny.(map[string]any); ok {
title, titleOk := unwrapToString(webring["title"])
link, linkOk := unwrapToString(webring["link"])
prev, prevOk := unwrapToString(webring["prev"])
next, nextOk := unwrapToString(webring["next"])
if titleOk && (linkOk || prevOk || nextOk) {
buf.Reset()
hb.WriteElementOpen("p")
if prevOk {
hb.WriteElementOpen("a", "href", prev)
hb.WriteEscaped("←")
hb.WriteElementClose("a")
hb.WriteEscaped(" ")
}
if linkOk {
hb.WriteElementOpen("a", "href", link)
}
hb.WriteEscaped(title)
if linkOk {
hb.WriteElementClose("a")
}
if nextOk {
hb.WriteEscaped(" ")
hb.WriteElementOpen("a", "href", next)
hb.WriteEscaped("→")
hb.WriteElementClose("a")
}
hb.WriteElementClose("p")
doc.Find("footer").AppendHtml(buf.String())
}
}
}
}
}
_ = goquery.Render(modified, doc.Selection)
}
func unwrapToString(o any) (string, bool) {

View File

@ -11,35 +11,16 @@ import (
var _ plugintypes.App = &goBlog{}
var _ plugintypes.Database = &database{}
var _ plugintypes.Post = &post{}
var _ plugintypes.PostRenderData = &pluginPostRenderData{}
var _ plugintypes.RenderContext = &pluginRenderContext{}
func TestExecPlugin(t *testing.T) {
func TestDemoPlugin(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
app.cfg.Plugins = []*configPlugin{
{
Path: "./plugins/demo",
Type: "exec",
Import: "demoexec",
},
}
err := app.initConfig(false)
require.NoError(t, err)
err = app.initPlugins()
require.NoError(t, err)
}
func TestMiddlewarePlugin(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
app.cfg.Plugins = []*configPlugin{
{
Path: "./plugins/demo",
Type: "middleware",
Import: "demomiddleware",
Path: "embedded:demo",
Import: "demo",
Config: map[string]any{
"prio": 99,
},
@ -51,10 +32,9 @@ func TestMiddlewarePlugin(t *testing.T) {
err = app.initPlugins()
require.NoError(t, err)
middlewarePlugins := getPluginsForType[plugintypes.Middleware](app, "middleware")
middlewarePlugins := app.getPlugins(pluginMiddlewareType)
if assert.Len(t, middlewarePlugins, 1) {
mdw := middlewarePlugins[0]
mdw := middlewarePlugins[0].(plugintypes.Middleware)
assert.Equal(t, 99, mdw.Prio())
}
}

View File

@ -4,8 +4,11 @@ import (
"io"
"net/http"
"github.com/samber/lo"
"go.goblog.app/app/pkgs/bufferpool"
"go.goblog.app/app/pkgs/contenttype"
"go.goblog.app/app/pkgs/htmlbuilder"
"go.goblog.app/app/pkgs/plugintypes"
)
type renderData struct {
@ -39,12 +42,26 @@ func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, st
// Write status code
w.WriteHeader(statusCode)
// Render
pipeReader, pipeWriter := io.Pipe()
go func() {
f(htmlbuilder.NewHtmlBuilder(pipeWriter), data)
_ = pipeWriter.Close()
}()
_ = pipeReader.CloseWithError(a.min.Get().Minify(contenttype.HTML, w, pipeReader))
buf := bufferpool.Get()
defer bufferpool.Put(buf)
f(htmlbuilder.NewHtmlBuilder(buf), data)
// Check if UI plugins are registered
uiPlugins := a.getPlugins(pluginUiType)
if len(uiPlugins) > 0 {
pluginBuf := bufferpool.Get()
defer bufferpool.Put(pluginBuf)
for _, plug := range lo.Reverse(uiPlugins) {
pluginBuf.Reset()
plug.(plugintypes.UI).Render(&pluginRenderContext{
blog: data.BlogString,
path: r.URL.Path,
}, buf, pluginBuf)
buf.Reset()
_, _ = io.Copy(buf, pluginBuf)
}
}
// Return minified HTML
_ = a.min.Get().Minify(contenttype.HTML, w, buf)
}
func (a *goBlog) checkRenderData(r *http.Request, data *renderData) {
@ -89,3 +106,18 @@ func (a *goBlog) checkRenderData(r *http.Request, data *renderData) {
data.Data = map[string]any{}
}
}
// Plugins
type pluginRenderContext struct {
blog string
path string
}
func (d *pluginRenderContext) GetBlog() string {
return d.blog
}
func (d *pluginRenderContext) GetPath() string {
return d.path
}

161
ui.go
View File

@ -10,31 +10,8 @@ import (
"github.com/samber/lo"
"go.goblog.app/app/pkgs/contenttype"
"go.goblog.app/app/pkgs/htmlbuilder"
"go.goblog.app/app/pkgs/plugintypes"
)
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")
@ -164,34 +141,32 @@ func (a *goBlog) renderBase(hb *htmlbuilder.HtmlBuilder, rd *renderData, title,
}
// Footer
hb.WriteElementOpen("footer")
a.renderWithPlugins(hb, plugintypes.BlogFooterRenderType, rd.Blog.pluginRenderData(), func(hb *htmlbuilder.HtmlBuilder) {
// Footer menu
if fm, ok := rd.Blog.Menus["footer"]; ok {
hb.WriteElementOpen("nav")
for i, item := range fm.Items {
if i > 0 {
hb.WriteUnescaped(" • ")
}
hb.WriteElementOpen("a", "href", item.Link)
hb.WriteEscaped(a.renderMdTitle(item.Title))
hb.WriteElementClose("a")
// Footer menu
if fm, ok := rd.Blog.Menus["footer"]; ok {
hb.WriteElementOpen("nav")
for i, item := range fm.Items {
if i > 0 {
hb.WriteUnescaped(" • ")
}
hb.WriteElementClose("nav")
hb.WriteElementOpen("a", "href", item.Link)
hb.WriteEscaped(a.renderMdTitle(item.Title))
hb.WriteElementClose("a")
}
// Copyright
hb.WriteElementOpen("p", "translate", "no")
hb.WriteUnescaped("© ")
hb.WriteEscaped(time.Now().Format("2006"))
hb.WriteUnescaped(" ")
if user != nil && user.Name != "" {
hb.WriteEscaped(user.Name)
} else {
hb.WriteEscaped(renderedBlogTitle)
}
hb.WriteElementClose("p")
// Tor
a.renderTorNotice(hb, rd)
})
hb.WriteElementClose("nav")
}
// Copyright
hb.WriteElementOpen("p", "translate", "no")
hb.WriteUnescaped("© ")
hb.WriteEscaped(time.Now().Format("2006"))
hb.WriteUnescaped(" ")
if user != nil && user.Name != "" {
hb.WriteEscaped(user.Name)
} else {
hb.WriteEscaped(renderedBlogTitle)
}
hb.WriteElementClose("p")
// Tor
a.renderTorNotice(hb, rd)
hb.WriteElementClose("footer")
// Easter egg
if rd.EasterEgg {
@ -900,52 +875,50 @@ func (a *goBlog) renderPost(hb *htmlbuilder.HtmlBuilder, rd *renderData) {
},
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
a.renderShareButton(hb, p, rd.Blog)
// Translate button
a.renderTranslateButton(hb, p, rd.Blog)
// 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
// 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
a.renderShareButton(hb, p, rd.Blog)
// Translate button
a.renderTranslateButton(hb, p, rd.Blog)
// 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")
// 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
a.postHtmlToWriter(hb, &postHtmlOptions{p: p})
// 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)
})
}
// Old content warning
a.renderOldContentWarning(hb, p, rd.Blog)
// Content
a.postHtmlToWriter(hb, &postHtmlOptions{p: p})
// 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)