mirror of https://github.com/jlelse/GoBlog
parent
2158b156c5
commit
a45d28d04f
|
@ -59,3 +59,4 @@ Here's an (incomplete) list of features:
|
||||||
- [How to configure GoBlog](./config.md)
|
- [How to configure GoBlog](./config.md)
|
||||||
- [Administration paths](./admin-paths.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/bufferpool"
|
||||||
"go.goblog.app/app/pkgs/contenttype"
|
"go.goblog.app/app/pkgs/contenttype"
|
||||||
|
"go.goblog.app/app/pkgs/htmlbuilder"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
ws "nhooyr.io/websocket"
|
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)
|
p.RenderedTitle = a.renderMdTitle(t)
|
||||||
}
|
}
|
||||||
// Render post (using post's blog config)
|
// Render post (using post's blog config)
|
||||||
hb := newHtmlBuilder(w)
|
hb := htmlbuilder.NewHtmlBuilder(w)
|
||||||
a.renderEditorPreview(hb, a.cfg.Blogs[p.Blog], p)
|
a.renderEditorPreview(hb, a.cfg.Blogs[p.Blog], p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -59,7 +59,7 @@ require (
|
||||||
// master
|
// master
|
||||||
github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38
|
github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38
|
||||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
|
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/sync v0.0.0-20220722155255-886fb9371eb4
|
||||||
golang.org/x/text v0.3.7
|
golang.org/x/text v0.3.7
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
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-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-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-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-20220811182439-13a9a731de15 h1:cik0bxZUSJVDyaHf1hZPSDsU8SZHGQZQMeueXCE7yBQ=
|
||||||
golang.org/x/net v0.0.0-20220805013720-a33c5aa5df48/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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)
|
h = h.Append(a.securityHeaders)
|
||||||
}
|
}
|
||||||
// Add plugin middlewares
|
// Add plugin middlewares
|
||||||
middlewarePlugins := getPluginsForType[plugintypes.Middleware](a, "middleware")
|
middlewarePlugins := getPluginsForType[plugintypes.Middleware](a, middlewarePlugin)
|
||||||
sort.Slice(middlewarePlugins, func(i, j int) bool {
|
sort.Slice(middlewarePlugins, func(i, j int) bool {
|
||||||
// Sort with descending prio
|
// Sort with descending prio
|
||||||
return middlewarePlugins[i].Prio() > middlewarePlugins[j].Prio()
|
return middlewarePlugins[i].Prio() > middlewarePlugins[j].Prio()
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/yuin/goldmark/util"
|
"github.com/yuin/goldmark/util"
|
||||||
"go.goblog.app/app/pkgs/bufferpool"
|
"go.goblog.app/app/pkgs/bufferpool"
|
||||||
"go.goblog.app/app/pkgs/highlighting"
|
"go.goblog.app/app/pkgs/highlighting"
|
||||||
|
"go.goblog.app/app/pkgs/htmlbuilder"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *goBlog) initMarkdown() {
|
func (a *goBlog) initMarkdown() {
|
||||||
|
@ -183,13 +184,13 @@ func (c *customRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
|
||||||
dest = resolved[0]
|
dest = resolved[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hb := newHtmlBuilder(w)
|
hb := htmlbuilder.NewHtmlBuilder(w)
|
||||||
hb.writeElementOpen("a", "href", dest)
|
hb.WriteElementOpen("a", "href", dest)
|
||||||
imgEls := []any{"src", dest, "alt", string(n.Text(source)), "loading", "lazy"}
|
imgEls := []any{"src", dest, "alt", string(n.Text(source)), "loading", "lazy"}
|
||||||
if len(n.Title) > 0 {
|
if len(n.Title) > 0 {
|
||||||
imgEls = append(imgEls, "title", string(n.Title))
|
imgEls = append(imgEls, "title", string(n.Title))
|
||||||
}
|
}
|
||||||
hb.writeElementOpen("img", imgEls...)
|
hb.WriteElementOpen("img", imgEls...)
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
return ast.WalkSkipChildren, nil
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Interface to GoBlog
|
"go.goblog.app/app/pkgs/htmlbuilder"
|
||||||
|
)
|
||||||
|
|
||||||
// App is used to access GoBlog's app instance.
|
// App is used to access GoBlog's app instance.
|
||||||
type App interface {
|
type App interface {
|
||||||
|
@ -23,29 +22,27 @@ type Database interface {
|
||||||
QueryRowContext(context.Context, string, ...any) (*sql.Row, error)
|
QueryRowContext(context.Context, string, ...any) (*sql.Row, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plugin types
|
// Post
|
||||||
|
type Post interface {
|
||||||
// SetApp is used in all plugin types to allow
|
GetParameters() map[string][]string
|
||||||
// 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
|
// RenderType
|
||||||
// GoBlog set plugin configuration.
|
type RenderType string
|
||||||
type SetConfig interface {
|
|
||||||
SetConfig(map[string]any)
|
// RenderData
|
||||||
|
type RenderData interface {
|
||||||
|
// Empty
|
||||||
}
|
}
|
||||||
|
|
||||||
type Exec interface {
|
// RenderNextFunc
|
||||||
SetApp
|
type RenderNextFunc func(*htmlbuilder.HtmlBuilder)
|
||||||
SetConfig
|
|
||||||
Exec()
|
|
||||||
}
|
|
||||||
|
|
||||||
type Middleware interface {
|
// Render main element content on post page, data = PostRenderData
|
||||||
SetApp
|
const PostMainElementRenderType RenderType = "post-main-content"
|
||||||
SetConfig
|
|
||||||
Handler(http.Handler) http.Handler
|
// PostRenderData is RenderData containing a Post
|
||||||
Prio() int
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"go.goblog.app/app/pkgs/htmlbuilder"
|
||||||
"go.goblog.app/app/pkgs/plugintypes"
|
"go.goblog.app/app/pkgs/plugintypes"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
@ -34,21 +35,34 @@ import (
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Symbols["go.goblog.app/app/pkgs/plugintypes/plugintypes"] = map[string]reflect.Value{
|
Symbols["go.goblog.app/app/pkgs/plugintypes/plugintypes"] = map[string]reflect.Value{
|
||||||
|
// function, constant and variable definitions
|
||||||
|
"PostMainElementRenderType": reflect.ValueOf(plugintypes.PostMainElementRenderType),
|
||||||
|
|
||||||
// type definitions
|
// type definitions
|
||||||
"App": reflect.ValueOf((*plugintypes.App)(nil)),
|
"App": reflect.ValueOf((*plugintypes.App)(nil)),
|
||||||
"Database": reflect.ValueOf((*plugintypes.Database)(nil)),
|
"Database": reflect.ValueOf((*plugintypes.Database)(nil)),
|
||||||
"Exec": reflect.ValueOf((*plugintypes.Exec)(nil)),
|
"Exec": reflect.ValueOf((*plugintypes.Exec)(nil)),
|
||||||
"Middleware": reflect.ValueOf((*plugintypes.Middleware)(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)),
|
"SetApp": reflect.ValueOf((*plugintypes.SetApp)(nil)),
|
||||||
"SetConfig": reflect.ValueOf((*plugintypes.SetConfig)(nil)),
|
"SetConfig": reflect.ValueOf((*plugintypes.SetConfig)(nil)),
|
||||||
|
"UI": reflect.ValueOf((*plugintypes.UI)(nil)),
|
||||||
|
|
||||||
// interface wrapper definitions
|
// interface wrapper definitions
|
||||||
"_App": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_App)(nil)),
|
"_App": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_App)(nil)),
|
||||||
"_Database": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Database)(nil)),
|
"_Database": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Database)(nil)),
|
||||||
"_Exec": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Exec)(nil)),
|
"_Exec": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Exec)(nil)),
|
||||||
"_Middleware": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Middleware)(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)),
|
"_SetApp": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetApp)(nil)),
|
||||||
"_SetConfig": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetConfig)(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)
|
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
|
// _go_goblog_app_app_pkgs_plugintypes_SetApp is an interface wrapper for SetApp type
|
||||||
type _go_goblog_app_app_pkgs_plugintypes_SetApp struct {
|
type _go_goblog_app_app_pkgs_plugintypes_SetApp struct {
|
||||||
IValue interface{}
|
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) {
|
func (W _go_goblog_app_app_pkgs_plugintypes_SetConfig) SetConfig(a0 map[string]any) {
|
||||||
W.WSetConfig(a0)
|
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/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"
|
"go.goblog.app/app/pkgs/yaegiwrappers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
execPlugin = "exec"
|
||||||
|
middlewarePlugin = "middleware"
|
||||||
|
uiPlugin = "ui"
|
||||||
|
)
|
||||||
|
|
||||||
func (a *goBlog) initPlugins() error {
|
func (a *goBlog) initPlugins() error {
|
||||||
a.pluginHost = plugins.NewPluginHost(yaegiwrappers.Symbols)
|
a.pluginHost = plugins.NewPluginHost(yaegiwrappers.Symbols)
|
||||||
|
|
||||||
a.pluginHost.AddPluginType("exec", (*plugintypes.Exec)(nil))
|
a.pluginHost.AddPluginType(execPlugin, (*plugintypes.Exec)(nil))
|
||||||
a.pluginHost.AddPluginType("middleware", (*plugintypes.Middleware)(nil))
|
a.pluginHost.AddPluginType(middlewarePlugin, (*plugintypes.Middleware)(nil))
|
||||||
|
a.pluginHost.AddPluginType(uiPlugin, (*plugintypes.UI)(nil))
|
||||||
|
|
||||||
for _, pc := range a.cfg.Plugins {
|
for _, pc := range a.cfg.Plugins {
|
||||||
if pluginInterface, err := a.pluginHost.LoadPlugin(&plugins.PluginConfig{
|
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 {
|
for _, p := range execs {
|
||||||
go p.Exec()
|
go p.Exec()
|
||||||
}
|
}
|
||||||
|
@ -38,15 +45,30 @@ func (a *goBlog) initPlugins() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPluginsForType[T any](a *goBlog, pluginType string) (list []T) {
|
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)
|
return plugins.GetPluginsForType[T](a.pluginHost, pluginType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement all needed interfaces for goblog
|
// Implement all needed interfaces
|
||||||
|
|
||||||
var _ plugintypes.App = &goBlog{}
|
|
||||||
|
|
||||||
func (a *goBlog) GetDatabase() plugintypes.Database {
|
func (a *goBlog) GetDatabase() plugintypes.Database {
|
||||||
return a.db
|
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"
|
"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) {
|
func TestExecPlugin(t *testing.T) {
|
||||||
app := &goBlog{
|
app := &goBlog{
|
||||||
cfg: createDefaultTestConfig(t),
|
cfg: createDefaultTestConfig(t),
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
gogeouri "git.jlel.se/jlelse/go-geouri"
|
gogeouri "git.jlel.se/jlelse/go-geouri"
|
||||||
"github.com/araddon/dateparse"
|
"github.com/araddon/dateparse"
|
||||||
"go.goblog.app/app/pkgs/bufferpool"
|
"go.goblog.app/app/pkgs/bufferpool"
|
||||||
|
"go.goblog.app/app/pkgs/htmlbuilder"
|
||||||
"gopkg.in/yaml.v3"
|
"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) {
|
func (a *goBlog) postHtmlToWriter(w io.Writer, p *post, absolute bool) {
|
||||||
// Build HTML
|
// Build HTML
|
||||||
hb := newHtmlBuilder(w)
|
hb := htmlbuilder.NewHtmlBuilder(w)
|
||||||
// Add audio to the top
|
// Add audio to the top
|
||||||
for _, a := range p.Parameters[a.cfg.Micropub.AudioParam] {
|
for _, a := range p.Parameters[a.cfg.Micropub.AudioParam] {
|
||||||
hb.writeElementOpen("audio", "controls", "preload", "none")
|
hb.WriteElementOpen("audio", "controls", "preload", "none")
|
||||||
hb.writeElementOpen("source", "src", a)
|
hb.WriteElementOpen("source", "src", a)
|
||||||
hb.writeElementClose("source")
|
hb.WriteElementClose("source")
|
||||||
hb.writeElementClose("audio")
|
hb.WriteElementClose("audio")
|
||||||
}
|
}
|
||||||
// Render markdown
|
// Render markdown
|
||||||
_ = a.renderMarkdownToWriter(w, p.Content, absolute)
|
_ = a.renderMarkdownToWriter(w, p.Content, absolute)
|
||||||
// Add bookmark links to the bottom
|
// Add bookmark links to the bottom
|
||||||
for _, l := range p.Parameters[a.cfg.Micropub.BookmarkParam] {
|
for _, l := range p.Parameters[a.cfg.Micropub.BookmarkParam] {
|
||||||
hb.writeElementOpen("p")
|
hb.WriteElementOpen("p")
|
||||||
hb.writeElementOpen("a", "class", "u-bookmark-of", "href", l, "target", "_blank", "rel", "noopener noreferrer")
|
hb.WriteElementOpen("a", "class", "u-bookmark-of", "href", l, "target", "_blank", "rel", "noopener noreferrer")
|
||||||
hb.writeEscaped(l)
|
hb.WriteEscaped(l)
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
hb.writeElementClose("p")
|
hb.WriteElementClose("p")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) feedHtml(w io.Writer, p *post) {
|
func (a *goBlog) feedHtml(w io.Writer, p *post) {
|
||||||
hb := newHtmlBuilder(w)
|
hb := htmlbuilder.NewHtmlBuilder(w)
|
||||||
// Add TTS audio to the top
|
// Add TTS audio to the top
|
||||||
for _, a := range p.Parameters[ttsParameter] {
|
for _, a := range p.Parameters[ttsParameter] {
|
||||||
hb.writeElementOpen("audio", "controls", "preload", "none")
|
hb.WriteElementOpen("audio", "controls", "preload", "none")
|
||||||
hb.writeElementOpen("source", "src", a)
|
hb.WriteElementOpen("source", "src", a)
|
||||||
hb.writeElementClose("source")
|
hb.WriteElementClose("source")
|
||||||
hb.writeElementClose("audio")
|
hb.WriteElementClose("audio")
|
||||||
}
|
}
|
||||||
// Add IndieWeb context
|
// Add IndieWeb context
|
||||||
a.renderPostReplyContext(hb, p, "p")
|
a.renderPostReplyContext(hb, p, "p")
|
||||||
|
@ -81,16 +82,16 @@ func (a *goBlog) feedHtml(w io.Writer, p *post) {
|
||||||
// Add link to interactions and comments
|
// Add link to interactions and comments
|
||||||
blogConfig := a.getBlogFromPost(p)
|
blogConfig := a.getBlogFromPost(p)
|
||||||
if cc := blogConfig.Comments; cc != nil && cc.Enabled {
|
if cc := blogConfig.Comments; cc != nil && cc.Enabled {
|
||||||
hb.writeElementOpen("p")
|
hb.WriteElementOpen("p")
|
||||||
hb.writeElementOpen("a", "href", a.getFullAddress(p.Path)+"#interactions")
|
hb.WriteElementOpen("a", "href", a.getFullAddress(p.Path)+"#interactions")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(blogConfig.Lang, "interactions"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(blogConfig.Lang, "interactions"))
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
hb.writeElementClose("p")
|
hb.WriteElementClose("p")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) minFeedHtml(w io.Writer, p *post) {
|
func (a *goBlog) minFeedHtml(w io.Writer, p *post) {
|
||||||
hb := newHtmlBuilder(w)
|
hb := htmlbuilder.NewHtmlBuilder(w)
|
||||||
// Add IndieWeb context
|
// Add IndieWeb context
|
||||||
a.renderPostReplyContext(hb, p, "p")
|
a.renderPostReplyContext(hb, p, "p")
|
||||||
a.renderPostLikeContext(hb, p, "p")
|
a.renderPostLikeContext(hb, p, "p")
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"go.goblog.app/app/pkgs/contenttype"
|
"go.goblog.app/app/pkgs/contenttype"
|
||||||
|
"go.goblog.app/app/pkgs/htmlbuilder"
|
||||||
)
|
)
|
||||||
|
|
||||||
type renderData struct {
|
type renderData struct {
|
||||||
|
@ -27,11 +28,11 @@ func (d *renderData) LoggedIn() bool {
|
||||||
return d.app.isLoggedIn(d.req)
|
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)
|
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
|
// Check render data
|
||||||
a.checkRenderData(r, data)
|
a.checkRenderData(r, data)
|
||||||
// Set content type
|
// Set content type
|
||||||
|
@ -41,7 +42,7 @@ func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, st
|
||||||
// Render
|
// Render
|
||||||
pipeReader, pipeWriter := io.Pipe()
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
go func() {
|
go func() {
|
||||||
f(newHtmlBuilder(pipeWriter), data)
|
f(htmlbuilder.NewHtmlBuilder(pipeWriter), data)
|
||||||
_ = pipeWriter.Close()
|
_ = pipeWriter.Close()
|
||||||
}()
|
}()
|
||||||
_ = pipeReader.CloseWithError(a.min.Get().Minify(contenttype.HTML, w, pipeReader))
|
_ = 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"
|
"github.com/samber/lo"
|
||||||
"go.goblog.app/app/pkgs/bufferpool"
|
"go.goblog.app/app/pkgs/bufferpool"
|
||||||
|
"go.goblog.app/app/pkgs/htmlbuilder"
|
||||||
)
|
)
|
||||||
|
|
||||||
type summaryTyp string
|
type summaryTyp string
|
||||||
|
@ -17,7 +18,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// post summary on index pages
|
// 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 {
|
if bc == nil || p == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -25,21 +26,21 @@ func (a *goBlog) renderSummary(hb *htmlBuilder, bc *configBlog, p *post, typ sum
|
||||||
typ = defaultSummary
|
typ = defaultSummary
|
||||||
}
|
}
|
||||||
// Start article
|
// Start article
|
||||||
hb.writeElementOpen("article", "class", "h-entry border-bottom")
|
hb.WriteElementOpen("article", "class", "h-entry border-bottom")
|
||||||
if p.Priority > 0 {
|
if p.Priority > 0 {
|
||||||
// Is pinned post
|
// Is pinned post
|
||||||
hb.writeElementOpen("p")
|
hb.WriteElementOpen("p")
|
||||||
hb.writeEscaped("📌 ")
|
hb.WriteEscaped("📌 ")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(bc.Lang, "pinned"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(bc.Lang, "pinned"))
|
||||||
hb.writeElementClose("p")
|
hb.WriteElementClose("p")
|
||||||
}
|
}
|
||||||
if p.RenderedTitle != "" {
|
if p.RenderedTitle != "" {
|
||||||
// Has title
|
// Has title
|
||||||
hb.writeElementOpen("h2", "class", "p-name")
|
hb.WriteElementOpen("h2", "class", "p-name")
|
||||||
hb.writeElementOpen("a", "class", "u-url", "href", p.Path)
|
hb.WriteElementOpen("a", "class", "u-url", "href", p.Path)
|
||||||
hb.writeEscaped(p.RenderedTitle)
|
hb.WriteEscaped(p.RenderedTitle)
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
hb.writeElementClose("h2")
|
hb.WriteElementClose("h2")
|
||||||
}
|
}
|
||||||
// Show photos in photo summary
|
// Show photos in photo summary
|
||||||
photos := a.photoLinks(p)
|
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")
|
a.renderPostMeta(hb, p, bc, "summary")
|
||||||
if typ != photoSummary && a.showFull(p) {
|
if typ != photoSummary && a.showFull(p) {
|
||||||
// Show full content
|
// Show full content
|
||||||
hb.writeElementOpen("div", "class", "e-content")
|
hb.WriteElementOpen("div", "class", "e-content")
|
||||||
a.postHtmlToWriter(hb, p, false)
|
a.postHtmlToWriter(hb, p, false)
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
} else {
|
} else {
|
||||||
// Show summary
|
// Show summary
|
||||||
hb.writeElementOpen("p", "class", "p-summary")
|
hb.WriteElementOpen("p", "class", "p-summary")
|
||||||
hb.writeEscaped(a.postSummary(p))
|
hb.WriteEscaped(a.postSummary(p))
|
||||||
hb.writeElementClose("p")
|
hb.WriteElementClose("p")
|
||||||
}
|
}
|
||||||
// Show link to full post
|
// Show link to full post
|
||||||
hb.writeElementOpen("p")
|
hb.WriteElementOpen("p")
|
||||||
prefix := bufferpool.Get()
|
prefix := bufferpool.Get()
|
||||||
if len(photos) > 0 {
|
if len(photos) > 0 {
|
||||||
// Contains photos
|
// Contains photos
|
||||||
|
@ -74,19 +75,19 @@ func (a *goBlog) renderSummary(hb *htmlBuilder, bc *configBlog, p *post, typ sum
|
||||||
}
|
}
|
||||||
if prefix.Len() > 0 {
|
if prefix.Len() > 0 {
|
||||||
prefix.WriteRune(' ')
|
prefix.WriteRune(' ')
|
||||||
hb.writeEscaped(prefix.String())
|
hb.WriteEscaped(prefix.String())
|
||||||
}
|
}
|
||||||
bufferpool.Put(prefix)
|
bufferpool.Put(prefix)
|
||||||
hb.writeElementOpen("a", "class", "u-url", "href", p.Path)
|
hb.WriteElementOpen("a", "class", "u-url", "href", p.Path)
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(bc.Lang, "view"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(bc.Lang, "view"))
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
hb.writeElementClose("p")
|
hb.WriteElementClose("p")
|
||||||
// Finish article
|
// Finish article
|
||||||
hb.writeElementClose("article")
|
hb.WriteElementClose("article")
|
||||||
}
|
}
|
||||||
|
|
||||||
// list of post taxonomy values (tags, series, etc.)
|
// 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 {
|
if b == nil || p == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -95,372 +96,372 @@ func (a *goBlog) renderPostTax(hb *htmlBuilder, p *post, b *configBlog) {
|
||||||
// Get all sorted taxonomy values for this post
|
// Get all sorted taxonomy values for this post
|
||||||
if taxValues := sortedStrings(p.Parameters[tax.Name]); len(taxValues) > 0 {
|
if taxValues := sortedStrings(p.Parameters[tax.Name]); len(taxValues) > 0 {
|
||||||
// Start new paragraph
|
// Start new paragraph
|
||||||
hb.writeElementOpen("p")
|
hb.WriteElementOpen("p")
|
||||||
// Add taxonomy name
|
// Add taxonomy name
|
||||||
hb.writeElementOpen("strong")
|
hb.WriteElementOpen("strong")
|
||||||
hb.writeEscaped(a.renderMdTitle(tax.Title))
|
hb.WriteEscaped(a.renderMdTitle(tax.Title))
|
||||||
hb.writeElementClose("strong")
|
hb.WriteElementClose("strong")
|
||||||
hb.write(": ")
|
hb.WriteUnescaped(": ")
|
||||||
// Add taxonomy values
|
// Add taxonomy values
|
||||||
for i, taxValue := range taxValues {
|
for i, taxValue := range taxValues {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
hb.write(", ")
|
hb.WriteUnescaped(", ")
|
||||||
}
|
}
|
||||||
hb.writeElementOpen(
|
hb.WriteElementOpen(
|
||||||
"a",
|
"a",
|
||||||
"class", "p-category",
|
"class", "p-category",
|
||||||
"rel", "tag",
|
"rel", "tag",
|
||||||
"href", b.getRelativePath(fmt.Sprintf("/%s/%s", tax.Name, urlize(taxValue))),
|
"href", b.getRelativePath(fmt.Sprintf("/%s/%s", tax.Name, urlize(taxValue))),
|
||||||
)
|
)
|
||||||
hb.writeEscaped(a.renderMdTitle(taxValue))
|
hb.WriteEscaped(a.renderMdTitle(taxValue))
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
}
|
}
|
||||||
// End paragraph
|
// End paragraph
|
||||||
hb.writeElementClose("p")
|
hb.WriteElementClose("p")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// post meta information.
|
// post meta information.
|
||||||
// typ can be "summary", "post" or "preview".
|
// 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" {
|
if b == nil || p == nil || typ != "summary" && typ != "post" && typ != "preview" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if typ == "summary" || typ == "post" {
|
if typ == "summary" || typ == "post" {
|
||||||
hb.writeElementOpen("div", "class", "p")
|
hb.WriteElementOpen("div", "class", "p")
|
||||||
}
|
}
|
||||||
// Published time
|
// Published time
|
||||||
if published := toLocalTime(p.Published); !published.IsZero() {
|
if published := toLocalTime(p.Published); !published.IsZero() {
|
||||||
hb.writeElementOpen("div")
|
hb.WriteElementOpen("div")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "publishedon"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "publishedon"))
|
||||||
hb.write(" ")
|
hb.WriteUnescaped(" ")
|
||||||
hb.writeElementOpen("time", "class", "dt-published", "datetime", published.Format(time.RFC3339))
|
hb.WriteElementOpen("time", "class", "dt-published", "datetime", published.Format(time.RFC3339))
|
||||||
hb.writeEscaped(published.Format(isoDateFormat))
|
hb.WriteEscaped(published.Format(isoDateFormat))
|
||||||
hb.writeElementClose("time")
|
hb.WriteElementClose("time")
|
||||||
// Section
|
// Section
|
||||||
if p.Section != "" {
|
if p.Section != "" {
|
||||||
if section := b.Sections[p.Section]; section != nil {
|
if section := b.Sections[p.Section]; section != nil {
|
||||||
hb.write(" in ") // TODO: Replace with a proper translation
|
hb.WriteUnescaped(" in ") // TODO: Replace with a proper translation
|
||||||
hb.writeElementOpen("a", "href", b.getRelativePath(section.Name))
|
hb.WriteElementOpen("a", "href", b.getRelativePath(section.Name))
|
||||||
hb.writeEscaped(a.renderMdTitle(section.Title))
|
hb.WriteEscaped(a.renderMdTitle(section.Title))
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
}
|
}
|
||||||
// Updated time
|
// Updated time
|
||||||
if updated := toLocalTime(p.Updated); !updated.IsZero() {
|
if updated := toLocalTime(p.Updated); !updated.IsZero() {
|
||||||
hb.writeElementOpen("div")
|
hb.WriteElementOpen("div")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "updatedon"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "updatedon"))
|
||||||
hb.write(" ")
|
hb.WriteUnescaped(" ")
|
||||||
hb.writeElementOpen("time", "class", "dt-updated", "datetime", updated.Format(time.RFC3339))
|
hb.WriteElementOpen("time", "class", "dt-updated", "datetime", updated.Format(time.RFC3339))
|
||||||
hb.writeEscaped(updated.Format(isoDateFormat))
|
hb.WriteEscaped(updated.Format(isoDateFormat))
|
||||||
hb.writeElementClose("time")
|
hb.WriteElementClose("time")
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
}
|
}
|
||||||
// IndieWeb Meta
|
// IndieWeb Meta
|
||||||
a.renderPostReplyContext(hb, p, "")
|
a.renderPostReplyContext(hb, p, "")
|
||||||
a.renderPostLikeContext(hb, p, "")
|
a.renderPostLikeContext(hb, p, "")
|
||||||
// Like ("u-like-of")
|
// Like ("u-like-of")
|
||||||
if likeLink := a.likeLink(p); likeLink != "" {
|
if likeLink := a.likeLink(p); likeLink != "" {
|
||||||
hb.writeElementOpen("div")
|
hb.WriteElementOpen("div")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "likeof"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "likeof"))
|
||||||
hb.writeEscaped(": ")
|
hb.WriteEscaped(": ")
|
||||||
hb.writeElementOpen("a", "class", "u-like-of", "rel", "noopener", "target", "_blank", "href", likeLink)
|
hb.WriteElementOpen("a", "class", "u-like-of", "rel", "noopener", "target", "_blank", "href", likeLink)
|
||||||
if likeTitle := a.likeTitle(p); likeTitle != "" {
|
if likeTitle := a.likeTitle(p); likeTitle != "" {
|
||||||
hb.writeEscaped(likeTitle)
|
hb.WriteEscaped(likeTitle)
|
||||||
} else {
|
} else {
|
||||||
hb.writeEscaped(likeLink)
|
hb.WriteEscaped(likeLink)
|
||||||
}
|
}
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
}
|
}
|
||||||
// Geo
|
// Geo
|
||||||
if geoURIs := a.geoURIs(p); len(geoURIs) != 0 {
|
if geoURIs := a.geoURIs(p); len(geoURIs) != 0 {
|
||||||
hb.writeElementOpen("div")
|
hb.WriteElementOpen("div")
|
||||||
hb.writeEscaped("📍 ")
|
hb.WriteEscaped("📍 ")
|
||||||
for i, geoURI := range geoURIs {
|
for i, geoURI := range geoURIs {
|
||||||
if i > 0 {
|
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("a", "class", "p-location h-geo", "target", "_blank", "rel", "nofollow noopener noreferrer", "href", geoOSMLink(geoURI))
|
||||||
hb.writeElementOpen("span", "class", "p-name")
|
hb.WriteElementOpen("span", "class", "p-name")
|
||||||
hb.writeEscaped(a.geoTitle(geoURI, b.Lang))
|
hb.WriteEscaped(a.geoTitle(geoURI, b.Lang))
|
||||||
hb.writeElementClose("span")
|
hb.WriteElementClose("span")
|
||||||
hb.writeElementOpen("data", "class", "p-longitude", "value", fmt.Sprintf("%f", geoURI.Longitude))
|
hb.WriteElementOpen("data", "class", "p-longitude", "value", fmt.Sprintf("%f", geoURI.Longitude))
|
||||||
hb.writeElementClose("data")
|
hb.WriteElementClose("data")
|
||||||
hb.writeElementOpen("data", "class", "p-latitude", "value", fmt.Sprintf("%f", geoURI.Latitude))
|
hb.WriteElementOpen("data", "class", "p-latitude", "value", fmt.Sprintf("%f", geoURI.Latitude))
|
||||||
hb.writeElementClose("data")
|
hb.WriteElementClose("data")
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
}
|
}
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
}
|
}
|
||||||
// Post specific elements
|
// Post specific elements
|
||||||
if typ == "post" {
|
if typ == "post" {
|
||||||
// Translations
|
// Translations
|
||||||
if translations := a.postTranslations(p); len(translations) > 0 {
|
if translations := a.postTranslations(p); len(translations) > 0 {
|
||||||
hb.writeElementOpen("div")
|
hb.WriteElementOpen("div")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "translations"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "translations"))
|
||||||
hb.writeEscaped(": ")
|
hb.WriteEscaped(": ")
|
||||||
for i, translation := range translations {
|
for i, translation := range translations {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
hb.writeEscaped(", ")
|
hb.WriteEscaped(", ")
|
||||||
}
|
}
|
||||||
hb.writeElementOpen("a", "translate", "no", "href", translation.Path)
|
hb.WriteElementOpen("a", "translate", "no", "href", translation.Path)
|
||||||
hb.writeEscaped(translation.RenderedTitle)
|
hb.WriteEscaped(translation.RenderedTitle)
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
}
|
}
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
}
|
}
|
||||||
// Short link
|
// Short link
|
||||||
if shortLink := a.shortPostURL(p); shortLink != "" {
|
if shortLink := a.shortPostURL(p); shortLink != "" {
|
||||||
hb.writeElementOpen("div")
|
hb.WriteElementOpen("div")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "shorturl"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "shorturl"))
|
||||||
hb.writeEscaped(" ")
|
hb.WriteEscaped(" ")
|
||||||
hb.writeElementOpen("a", "rel", "shortlink", "href", shortLink)
|
hb.WriteElementOpen("a", "rel", "shortlink", "href", shortLink)
|
||||||
hb.writeEscaped(shortLink)
|
hb.WriteEscaped(shortLink)
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
}
|
}
|
||||||
// Status
|
// Status
|
||||||
if p.Status != statusPublished {
|
if p.Status != statusPublished {
|
||||||
hb.writeElementOpen("div")
|
hb.WriteElementOpen("div")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "status"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "status"))
|
||||||
hb.writeEscaped(": ")
|
hb.WriteEscaped(": ")
|
||||||
hb.writeEscaped(string(p.Status))
|
hb.WriteEscaped(string(p.Status))
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if typ == "summary" || typ == "post" {
|
if typ == "summary" || typ == "post" {
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reply ("u-in-reply-to")
|
// 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 == "" {
|
if htmlWrapperElement == "" {
|
||||||
htmlWrapperElement = "div"
|
htmlWrapperElement = "div"
|
||||||
}
|
}
|
||||||
if replyLink := a.replyLink(p); replyLink != "" {
|
if replyLink := a.replyLink(p); replyLink != "" {
|
||||||
hb.writeElementOpen(htmlWrapperElement)
|
hb.WriteElementOpen(htmlWrapperElement)
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(a.getBlogFromPost(p).Lang, "replyto"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(a.getBlogFromPost(p).Lang, "replyto"))
|
||||||
hb.writeEscaped(": ")
|
hb.WriteEscaped(": ")
|
||||||
hb.writeElementOpen("a", "class", "u-in-reply-to", "rel", "noopener", "target", "_blank", "href", replyLink)
|
hb.WriteElementOpen("a", "class", "u-in-reply-to", "rel", "noopener", "target", "_blank", "href", replyLink)
|
||||||
if replyTitle := a.replyTitle(p); replyTitle != "" {
|
if replyTitle := a.replyTitle(p); replyTitle != "" {
|
||||||
hb.writeEscaped(replyTitle)
|
hb.WriteEscaped(replyTitle)
|
||||||
} else {
|
} else {
|
||||||
hb.writeEscaped(replyLink)
|
hb.WriteEscaped(replyLink)
|
||||||
}
|
}
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
hb.writeElementClose(htmlWrapperElement)
|
hb.WriteElementClose(htmlWrapperElement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Like ("u-like-of")
|
// 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 == "" {
|
if htmlWrapperElement == "" {
|
||||||
htmlWrapperElement = "div"
|
htmlWrapperElement = "div"
|
||||||
}
|
}
|
||||||
if likeLink := a.likeLink(p); likeLink != "" {
|
if likeLink := a.likeLink(p); likeLink != "" {
|
||||||
hb.writeElementOpen(htmlWrapperElement)
|
hb.WriteElementOpen(htmlWrapperElement)
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(a.getBlogFromPost(p).Lang, "likeof"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(a.getBlogFromPost(p).Lang, "likeof"))
|
||||||
hb.writeEscaped(": ")
|
hb.WriteEscaped(": ")
|
||||||
hb.writeElementOpen("a", "class", "u-like-of", "rel", "noopener", "target", "_blank", "href", likeLink)
|
hb.WriteElementOpen("a", "class", "u-like-of", "rel", "noopener", "target", "_blank", "href", likeLink)
|
||||||
if likeTitle := a.likeTitle(p); likeTitle != "" {
|
if likeTitle := a.likeTitle(p); likeTitle != "" {
|
||||||
hb.writeEscaped(likeTitle)
|
hb.WriteEscaped(likeTitle)
|
||||||
} else {
|
} else {
|
||||||
hb.writeEscaped(likeLink)
|
hb.WriteEscaped(likeLink)
|
||||||
}
|
}
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
hb.writeElementClose(htmlWrapperElement)
|
hb.WriteElementClose(htmlWrapperElement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// warning for old posts
|
// 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() {
|
if b == nil || b.hideOldContentWarning || p == nil || !p.Old() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hb.writeElementOpen("strong", "class", "p border-top border-bottom")
|
hb.WriteElementOpen("strong", "class", "p border-top border-bottom")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "oldcontent"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "oldcontent"))
|
||||||
hb.writeElementClose("strong")
|
hb.WriteElementClose("strong")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) renderInteractions(hb *htmlBuilder, rd *renderData) {
|
func (a *goBlog) renderInteractions(hb *htmlbuilder.HtmlBuilder, rd *renderData) {
|
||||||
// Start accordion
|
// Start accordion
|
||||||
hb.writeElementOpen("details", "class", "p", "id", "interactions")
|
hb.WriteElementOpen("details", "class", "p", "id", "interactions")
|
||||||
hb.writeElementOpen("summary")
|
hb.WriteElementOpen("summary")
|
||||||
hb.writeElementOpen("strong")
|
hb.WriteElementOpen("strong")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "interactions"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "interactions"))
|
||||||
hb.writeElementClose("strong")
|
hb.WriteElementClose("strong")
|
||||||
hb.writeElementClose("summary")
|
hb.WriteElementClose("summary")
|
||||||
// Render mentions
|
// Render mentions
|
||||||
var renderMentions func(m []*mention)
|
var renderMentions func(m []*mention)
|
||||||
renderMentions = func(m []*mention) {
|
renderMentions = func(m []*mention) {
|
||||||
if len(m) == 0 {
|
if len(m) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hb.writeElementOpen("ul")
|
hb.WriteElementOpen("ul")
|
||||||
for _, mention := range m {
|
for _, mention := range m {
|
||||||
hb.writeElementOpen("li")
|
hb.WriteElementOpen("li")
|
||||||
hb.writeElementOpen("a", "href", mention.Url, "target", "_blank", "rel", "nofollow noopener noreferrer ugc")
|
hb.WriteElementOpen("a", "href", mention.Url, "target", "_blank", "rel", "nofollow noopener noreferrer ugc")
|
||||||
hb.writeEscaped(defaultIfEmpty(mention.Author, mention.Url))
|
hb.WriteEscaped(defaultIfEmpty(mention.Author, mention.Url))
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
if mention.Title != "" {
|
if mention.Title != "" {
|
||||||
hb.write(" ")
|
hb.WriteUnescaped(" ")
|
||||||
hb.writeElementOpen("strong")
|
hb.WriteElementOpen("strong")
|
||||||
hb.writeEscaped(mention.Title)
|
hb.WriteEscaped(mention.Title)
|
||||||
hb.writeElementClose("strong")
|
hb.WriteElementClose("strong")
|
||||||
}
|
}
|
||||||
if mention.Content != "" {
|
if mention.Content != "" {
|
||||||
hb.write(" ")
|
hb.WriteUnescaped(" ")
|
||||||
hb.writeElementOpen("i")
|
hb.WriteElementOpen("i")
|
||||||
hb.writeEscaped(mention.Content)
|
hb.WriteEscaped(mention.Content)
|
||||||
hb.writeElementClose("i")
|
hb.WriteElementClose("i")
|
||||||
}
|
}
|
||||||
if len(mention.Submentions) > 0 {
|
if len(mention.Submentions) > 0 {
|
||||||
renderMentions(mention.Submentions)
|
renderMentions(mention.Submentions)
|
||||||
}
|
}
|
||||||
hb.writeElementClose("li")
|
hb.WriteElementClose("li")
|
||||||
}
|
}
|
||||||
hb.writeElementClose("ul")
|
hb.WriteElementClose("ul")
|
||||||
}
|
}
|
||||||
renderMentions(a.db.getWebmentionsByAddress(rd.Canonical))
|
renderMentions(a.db.getWebmentionsByAddress(rd.Canonical))
|
||||||
// Show form to send a webmention
|
// Show form to send a webmention
|
||||||
hb.writeElementOpen("form", "class", "fw p", "method", "post", "action", "/webmention")
|
hb.WriteElementOpen("form", "class", "fw p", "method", "post", "action", "/webmention")
|
||||||
hb.writeElementOpen("label", "for", "wm-source", "class", "p")
|
hb.WriteElementOpen("label", "for", "wm-source", "class", "p")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "interactionslabel"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "interactionslabel"))
|
||||||
hb.writeElementClose("label")
|
hb.WriteElementClose("label")
|
||||||
hb.writeElementOpen("input", "id", "wm-source", "type", "url", "name", "source", "placeholder", "URL", "required", "")
|
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", "hidden", "name", "target", "value", rd.Canonical)
|
||||||
hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "send"))
|
hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "send"))
|
||||||
hb.writeElementClose("form")
|
hb.WriteElementClose("form")
|
||||||
// Show form to create a new comment
|
// Show form to create a new comment
|
||||||
hb.writeElementOpen("form", "class", "fw p", "method", "post", "action", "/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", "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", "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("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.WriteElementOpen("textarea", "name", "comment", "required", "", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "comment"))
|
||||||
hb.writeElementClose("textarea")
|
hb.WriteElementClose("textarea")
|
||||||
hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "docomment"))
|
hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "docomment"))
|
||||||
hb.writeElementClose("form")
|
hb.WriteElementClose("form")
|
||||||
// Finish accordion
|
// Finish accordion
|
||||||
hb.writeElementClose("details")
|
hb.WriteElementClose("details")
|
||||||
}
|
}
|
||||||
|
|
||||||
// author h-card
|
// author h-card
|
||||||
func (a *goBlog) renderAuthor(hb *htmlBuilder) {
|
func (a *goBlog) renderAuthor(hb *htmlbuilder.HtmlBuilder) {
|
||||||
user := a.cfg.User
|
user := a.cfg.User
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hb.writeElementOpen("div", "class", "p-author h-card hide")
|
hb.WriteElementOpen("div", "class", "p-author h-card hide")
|
||||||
if user.Picture != "" {
|
if user.Picture != "" {
|
||||||
hb.writeElementOpen("data", "class", "u-photo", "value", user.Picture)
|
hb.WriteElementOpen("data", "class", "u-photo", "value", user.Picture)
|
||||||
hb.writeElementClose("data")
|
hb.WriteElementClose("data")
|
||||||
}
|
}
|
||||||
if user.Name != "" {
|
if user.Name != "" {
|
||||||
hb.writeElementOpen("a", "class", "p-name u-url", "rel", "me", "href", defaultIfEmpty(user.Link, "/"))
|
hb.WriteElementOpen("a", "class", "p-name u-url", "rel", "me", "href", defaultIfEmpty(user.Link, "/"))
|
||||||
hb.writeEscaped(user.Name)
|
hb.WriteEscaped(user.Name)
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
}
|
}
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
}
|
}
|
||||||
|
|
||||||
// head meta tags for a post
|
// 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 {
|
if p == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if canonical != "" {
|
if canonical != "" {
|
||||||
hb.writeElementOpen("meta", "property", "og:url", "content", canonical)
|
hb.WriteElementOpen("meta", "property", "og:url", "content", canonical)
|
||||||
hb.writeElementOpen("meta", "property", "twitter:url", "content", canonical)
|
hb.WriteElementOpen("meta", "property", "twitter:url", "content", canonical)
|
||||||
}
|
}
|
||||||
if p.RenderedTitle != "" {
|
if p.RenderedTitle != "" {
|
||||||
hb.writeElementOpen("meta", "property", "og:title", "content", p.RenderedTitle)
|
hb.WriteElementOpen("meta", "property", "og:title", "content", p.RenderedTitle)
|
||||||
hb.writeElementOpen("meta", "property", "twitter:title", "content", p.RenderedTitle)
|
hb.WriteElementOpen("meta", "property", "twitter:title", "content", p.RenderedTitle)
|
||||||
}
|
}
|
||||||
if summary := a.postSummary(p); summary != "" {
|
if summary := a.postSummary(p); summary != "" {
|
||||||
hb.writeElementOpen("meta", "name", "description", "content", summary)
|
hb.WriteElementOpen("meta", "name", "description", "content", summary)
|
||||||
hb.writeElementOpen("meta", "property", "og:description", "content", summary)
|
hb.WriteElementOpen("meta", "property", "og:description", "content", summary)
|
||||||
hb.writeElementOpen("meta", "property", "twitter:description", "content", summary)
|
hb.WriteElementOpen("meta", "property", "twitter:description", "content", summary)
|
||||||
}
|
}
|
||||||
if published := toLocalTime(p.Published); !published.IsZero() {
|
if published := toLocalTime(p.Published); !published.IsZero() {
|
||||||
hb.writeElementOpen("meta", "itemprop", "datePublished", "content", published.Format(time.RFC3339))
|
hb.WriteElementOpen("meta", "itemprop", "datePublished", "content", published.Format(time.RFC3339))
|
||||||
}
|
}
|
||||||
if updated := toLocalTime(p.Updated); !updated.IsZero() {
|
if updated := toLocalTime(p.Updated); !updated.IsZero() {
|
||||||
hb.writeElementOpen("meta", "itemprop", "dateModified", "content", updated.Format(time.RFC3339))
|
hb.WriteElementOpen("meta", "itemprop", "dateModified", "content", updated.Format(time.RFC3339))
|
||||||
}
|
}
|
||||||
for _, img := range a.photoLinks(p) {
|
for _, img := range a.photoLinks(p) {
|
||||||
hb.writeElementOpen("meta", "itemprop", "image", "content", img)
|
hb.WriteElementOpen("meta", "itemprop", "image", "content", img)
|
||||||
hb.writeElementOpen("meta", "property", "og:image", "content", img)
|
hb.WriteElementOpen("meta", "property", "og:image", "content", img)
|
||||||
hb.writeElementOpen("meta", "property", "twitter:image", "content", img)
|
hb.WriteElementOpen("meta", "property", "twitter:image", "content", img)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TOR notice in the footer
|
// TOR notice in the footer
|
||||||
func (a *goBlog) renderTorNotice(hb *htmlBuilder, rd *renderData) {
|
func (a *goBlog) renderTorNotice(hb *htmlbuilder.HtmlBuilder, rd *renderData) {
|
||||||
if !a.cfg.Server.Tor || (!rd.TorUsed && rd.TorAddress == "") {
|
if !a.cfg.Server.Tor || (!rd.TorUsed && rd.TorAddress == "") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if rd.TorUsed {
|
if rd.TorUsed {
|
||||||
hb.writeElementOpen("p", "id", "tor")
|
hb.WriteElementOpen("p", "id", "tor")
|
||||||
hb.writeEscaped("🔐 ")
|
hb.WriteEscaped("🔐 ")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "connectedviator"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "connectedviator"))
|
||||||
hb.writeElementClose("p")
|
hb.WriteElementClose("p")
|
||||||
} else if rd.TorAddress != "" {
|
} else if rd.TorAddress != "" {
|
||||||
hb.writeElementOpen("p", "id", "tor")
|
hb.WriteElementOpen("p", "id", "tor")
|
||||||
hb.writeEscaped("🔓 ")
|
hb.WriteEscaped("🔓 ")
|
||||||
hb.writeElementOpen("a", "href", rd.TorAddress)
|
hb.WriteElementOpen("a", "href", rd.TorAddress)
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "connectviator"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "connectviator"))
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
hb.writeEscaped(" ")
|
hb.WriteEscaped(" ")
|
||||||
hb.writeElementOpen("a", "href", "https://www.torproject.org/", "target", "_blank", "rel", "nofollow noopener noreferrer")
|
hb.WriteElementOpen("a", "href", "https://www.torproject.org/", "target", "_blank", "rel", "nofollow noopener noreferrer")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "whatistor"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "whatistor"))
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
hb.writeElementClose("p")
|
hb.WriteElementClose("p")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) renderTitleTag(hb *htmlBuilder, blog *configBlog, optionalTitle string) {
|
func (a *goBlog) renderTitleTag(hb *htmlbuilder.HtmlBuilder, blog *configBlog, optionalTitle string) {
|
||||||
hb.writeElementOpen("title")
|
hb.WriteElementOpen("title")
|
||||||
if optionalTitle != "" {
|
if optionalTitle != "" {
|
||||||
hb.writeEscaped(optionalTitle)
|
hb.WriteEscaped(optionalTitle)
|
||||||
hb.writeEscaped(" - ")
|
hb.WriteEscaped(" - ")
|
||||||
}
|
}
|
||||||
hb.writeEscaped(a.renderMdTitle(blog.Title))
|
hb.WriteEscaped(a.renderMdTitle(blog.Title))
|
||||||
hb.writeElementClose("title")
|
hb.WriteElementClose("title")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) renderPagination(hb *htmlBuilder, blog *configBlog, hasPrev, hasNext bool, prev, next string) {
|
func (a *goBlog) renderPagination(hb *htmlbuilder.HtmlBuilder, blog *configBlog, hasPrev, hasNext bool, prev, next string) {
|
||||||
// Navigation
|
// Navigation
|
||||||
if hasPrev {
|
if hasPrev {
|
||||||
hb.writeElementOpen("p")
|
hb.WriteElementOpen("p")
|
||||||
hb.writeElementOpen("a", "href", prev) // TODO: rel=prev?
|
hb.WriteElementOpen("a", "href", prev) // TODO: rel=prev?
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(blog.Lang, "prev"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(blog.Lang, "prev"))
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
hb.writeElementClose("p")
|
hb.WriteElementClose("p")
|
||||||
}
|
}
|
||||||
if hasNext {
|
if hasNext {
|
||||||
hb.writeElementOpen("p")
|
hb.WriteElementOpen("p")
|
||||||
hb.writeElementOpen("a", "href", next) // TODO: rel=next?
|
hb.WriteElementOpen("a", "href", next) // TODO: rel=next?
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(blog.Lang, "next"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(blog.Lang, "next"))
|
||||||
hb.writeElementClose("a")
|
hb.WriteElementClose("a")
|
||||||
hb.writeElementClose("p")
|
hb.WriteElementClose("p")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*goBlog) renderPostTitle(hb *htmlBuilder, p *post) {
|
func (*goBlog) renderPostTitle(hb *htmlbuilder.HtmlBuilder, p *post) {
|
||||||
if p == nil || p.RenderedTitle == "" {
|
if p == nil || p.RenderedTitle == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hb.writeElementOpen("h1", "class", "p-name")
|
hb.WriteElementOpen("h1", "class", "p-name")
|
||||||
hb.writeEscaped(p.RenderedTitle)
|
hb.WriteEscaped(p.RenderedTitle)
|
||||||
hb.writeElementClose("h1")
|
hb.WriteElementClose("h1")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) renderPostGPX(hb *htmlBuilder, p *post, b *configBlog) {
|
func (a *goBlog) renderPostGPX(hb *htmlbuilder.HtmlBuilder, p *post, b *configBlog) {
|
||||||
if p == nil || !p.hasTrack() {
|
if p == nil || !p.hasTrack() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -469,174 +470,174 @@ func (a *goBlog) renderPostGPX(hb *htmlBuilder, p *post, b *configBlog) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Track stats
|
// Track stats
|
||||||
hb.writeElementOpen("p")
|
hb.WriteElementOpen("p")
|
||||||
if track.Name != "" {
|
if track.Name != "" {
|
||||||
hb.writeElementOpen("strong")
|
hb.WriteElementOpen("strong")
|
||||||
hb.writeEscaped(track.Name)
|
hb.WriteEscaped(track.Name)
|
||||||
hb.writeElementClose("strong")
|
hb.WriteElementClose("strong")
|
||||||
hb.write(" ")
|
hb.WriteUnescaped(" ")
|
||||||
}
|
}
|
||||||
if track.Kilometers != "" {
|
if track.Kilometers != "" {
|
||||||
hb.write("🏁 ")
|
hb.WriteUnescaped("🏁 ")
|
||||||
hb.writeEscaped(track.Kilometers)
|
hb.WriteEscaped(track.Kilometers)
|
||||||
hb.write(" ")
|
hb.WriteUnescaped(" ")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "kilometers"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(b.Lang, "kilometers"))
|
||||||
hb.write(" ")
|
hb.WriteUnescaped(" ")
|
||||||
}
|
}
|
||||||
if track.Hours != "" {
|
if track.Hours != "" {
|
||||||
hb.write("⏱ ")
|
hb.WriteUnescaped("⏱ ")
|
||||||
hb.writeEscaped(track.Hours)
|
hb.WriteEscaped(track.Hours)
|
||||||
}
|
}
|
||||||
hb.writeElementClose("p")
|
hb.WriteElementClose("p")
|
||||||
// Map (only show if it has features)
|
// Map (only show if it has features)
|
||||||
if track.hasMapFeatures() {
|
if track.hasMapFeatures() {
|
||||||
hb.writeElementOpen(
|
hb.WriteElementOpen(
|
||||||
"div", "id", "map", "class", "p",
|
"div", "id", "map", "class", "p",
|
||||||
"data-paths", track.PathsJSON,
|
"data-paths", track.PathsJSON,
|
||||||
"data-points", track.PointsJSON,
|
"data-points", track.PointsJSON,
|
||||||
"data-minzoom", track.MinZoom, "data-maxzoom", track.MaxZoom,
|
"data-minzoom", track.MinZoom, "data-maxzoom", track.MaxZoom,
|
||||||
"data-attribution", track.MapAttribution,
|
"data-attribution", track.MapAttribution,
|
||||||
)
|
)
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
hb.writeElementOpen("script", "defer", "", "src", a.assetFileName("js/geomap.js"))
|
hb.WriteElementOpen("script", "defer", "", "src", a.assetFileName("js/geomap.js"))
|
||||||
hb.writeElementClose("script")
|
hb.WriteElementClose("script")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) renderPostReactions(hb *htmlBuilder, p *post) {
|
func (a *goBlog) renderPostReactions(hb *htmlbuilder.HtmlBuilder, p *post) {
|
||||||
if !a.reactionsEnabledForPost(p) {
|
if !a.reactionsEnabledForPost(p) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hb.writeElementOpen("div", "id", "reactions", "class", "actions", "data-path", p.Path, "data-allowed", strings.Join(allowedReactions, ","))
|
hb.WriteElementOpen("div", "id", "reactions", "class", "actions", "data-path", p.Path, "data-allowed", strings.Join(allowedReactions, ","))
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
hb.writeElementOpen("script", "defer", "", "src", a.assetFileName("js/reactions.js"))
|
hb.WriteElementOpen("script", "defer", "", "src", a.assetFileName("js/reactions.js"))
|
||||||
hb.writeElementClose("script")
|
hb.WriteElementClose("script")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) renderPostVideo(hb *htmlBuilder, p *post) {
|
func (a *goBlog) renderPostVideo(hb *htmlbuilder.HtmlBuilder, p *post) {
|
||||||
if !p.hasVideoPlaylist() {
|
if !p.hasVideoPlaylist() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hb.writeElementOpen("div", "id", "video", "data-url", p.firstParameter(videoPlaylistParam))
|
hb.WriteElementOpen("div", "id", "video", "data-url", p.firstParameter(videoPlaylistParam))
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
hb.writeElementOpen("script", "defer", "", "src", a.assetFileName("js/video.js"))
|
hb.WriteElementOpen("script", "defer", "", "src", a.assetFileName("js/video.js"))
|
||||||
hb.writeElementClose("script")
|
hb.WriteElementClose("script")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) renderPostSectionSettings(hb *htmlBuilder, rd *renderData, srd *settingsRenderData) {
|
func (a *goBlog) renderPostSectionSettings(hb *htmlbuilder.HtmlBuilder, rd *renderData, srd *settingsRenderData) {
|
||||||
hb.writeElementOpen("h2")
|
hb.WriteElementOpen("h2")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "postsections"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "postsections"))
|
||||||
hb.writeElementClose("h2")
|
hb.WriteElementClose("h2")
|
||||||
|
|
||||||
// Update default section
|
// Update default section
|
||||||
hb.writeElementOpen("h3")
|
hb.WriteElementOpen("h3")
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "default"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "default"))
|
||||||
hb.writeElementClose("h3")
|
hb.WriteElementClose("h3")
|
||||||
|
|
||||||
hb.writeElementOpen("form", "class", "fw p", "method", "post")
|
hb.WriteElementOpen("form", "class", "fw p", "method", "post")
|
||||||
hb.writeElementOpen("select", "name", "defaultsection")
|
hb.WriteElementOpen("select", "name", "defaultsection")
|
||||||
for _, section := range srd.sections {
|
for _, section := range srd.sections {
|
||||||
hb.writeElementOpen("option", "value", section.Name, lo.If(section.Name == srd.defaultSection, "selected").Else(""), "")
|
hb.WriteElementOpen("option", "value", section.Name, lo.If(section.Name == srd.defaultSection, "selected").Else(""), "")
|
||||||
hb.writeEscaped(section.Name)
|
hb.WriteEscaped(section.Name)
|
||||||
hb.writeElementClose("option")
|
hb.WriteElementClose("option")
|
||||||
}
|
}
|
||||||
hb.writeElementClose("select")
|
hb.WriteElementClose("select")
|
||||||
hb.writeElementOpen(
|
hb.WriteElementOpen(
|
||||||
"input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update"),
|
"input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update"),
|
||||||
"formaction", rd.Blog.getRelativePath(settingsPath+settingsUpdateDefaultSectionPath),
|
"formaction", rd.Blog.getRelativePath(settingsPath+settingsUpdateDefaultSectionPath),
|
||||||
)
|
)
|
||||||
hb.writeElementClose("form")
|
hb.WriteElementClose("form")
|
||||||
|
|
||||||
for _, section := range srd.sections {
|
for _, section := range srd.sections {
|
||||||
hb.writeElementOpen("details")
|
hb.WriteElementOpen("details")
|
||||||
|
|
||||||
hb.writeElementOpen("summary")
|
hb.WriteElementOpen("summary")
|
||||||
hb.writeElementOpen("h3")
|
hb.WriteElementOpen("h3")
|
||||||
hb.writeEscaped(section.Name)
|
hb.WriteEscaped(section.Name)
|
||||||
hb.writeElementClose("h3")
|
hb.WriteElementClose("h3")
|
||||||
hb.writeElementClose("summary")
|
hb.WriteElementClose("summary")
|
||||||
|
|
||||||
hb.writeElementOpen("form", "class", "fw p", "method", "post")
|
hb.WriteElementOpen("form", "class", "fw p", "method", "post")
|
||||||
|
|
||||||
hb.writeElementOpen("input", "type", "hidden", "name", "sectionname", "value", section.Name)
|
hb.WriteElementOpen("input", "type", "hidden", "name", "sectionname", "value", section.Name)
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
hb.writeElementOpen("input", "type", "text", "name", "sectiontitle", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectiontitle"), "required", "", "value", section.Title)
|
hb.WriteElementOpen("input", "type", "text", "name", "sectiontitle", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectiontitle"), "required", "", "value", section.Title)
|
||||||
// Description
|
// Description
|
||||||
hb.writeElementOpen(
|
hb.WriteElementOpen(
|
||||||
"textarea",
|
"textarea",
|
||||||
"name", "sectiondescription",
|
"name", "sectiondescription",
|
||||||
"class", "monospace",
|
"class", "monospace",
|
||||||
"placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectiondescription"),
|
"placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectiondescription"),
|
||||||
)
|
)
|
||||||
hb.writeEscaped(section.Description)
|
hb.WriteEscaped(section.Description)
|
||||||
hb.writeElementClose("textarea")
|
hb.WriteElementClose("textarea")
|
||||||
// Path template
|
// Path template
|
||||||
hb.writeElementOpen("input", "type", "text", "name", "sectionpathtemplate", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectionpathtemplate"), "value", section.PathTemplate)
|
hb.WriteElementOpen("input", "type", "text", "name", "sectionpathtemplate", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectionpathtemplate"), "value", section.PathTemplate)
|
||||||
// Show full
|
// Show full
|
||||||
hb.writeElementOpen("input", "type", "checkbox", "name", "sectionshowfull", "id", "showfull-"+section.Name, lo.If(section.ShowFull, "checked").Else(""), "")
|
hb.WriteElementOpen("input", "type", "checkbox", "name", "sectionshowfull", "id", "showfull-"+section.Name, lo.If(section.ShowFull, "checked").Else(""), "")
|
||||||
hb.writeElementOpen("label", "for", "showfull-"+section.Name)
|
hb.WriteElementOpen("label", "for", "showfull-"+section.Name)
|
||||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectionshowfull"))
|
hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectionshowfull"))
|
||||||
hb.writeElementClose("label")
|
hb.WriteElementClose("label")
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
hb.writeElementOpen("div", "class", "p")
|
hb.WriteElementOpen("div", "class", "p")
|
||||||
// Update
|
// Update
|
||||||
hb.writeElementOpen(
|
hb.WriteElementOpen(
|
||||||
"input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update"),
|
"input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update"),
|
||||||
"formaction", rd.Blog.getRelativePath(settingsPath+settingsUpdateSectionPath),
|
"formaction", rd.Blog.getRelativePath(settingsPath+settingsUpdateSectionPath),
|
||||||
)
|
)
|
||||||
// Delete
|
// Delete
|
||||||
hb.writeElementOpen(
|
hb.WriteElementOpen(
|
||||||
"input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete"),
|
"input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete"),
|
||||||
"formaction", rd.Blog.getRelativePath(settingsPath+settingsDeleteSectionPath),
|
"formaction", rd.Blog.getRelativePath(settingsPath+settingsDeleteSectionPath),
|
||||||
"class", "confirm", "data-confirmmessage", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "confirmdelete"),
|
"class", "confirm", "data-confirmmessage", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "confirmdelete"),
|
||||||
)
|
)
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
|
|
||||||
hb.writeElementClose("form")
|
hb.WriteElementClose("form")
|
||||||
hb.writeElementClose("details")
|
hb.WriteElementClose("details")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new section
|
// Create new section
|
||||||
hb.writeElementOpen("form", "class", "fw p", "method", "post")
|
hb.WriteElementOpen("form", "class", "fw p", "method", "post")
|
||||||
// Name
|
// Name
|
||||||
hb.writeElementOpen("input", "type", "text", "name", "sectionname", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectionname"), "required", "")
|
hb.WriteElementOpen("input", "type", "text", "name", "sectionname", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectionname"), "required", "")
|
||||||
// Title
|
// Title
|
||||||
hb.writeElementOpen("input", "type", "text", "name", "sectiontitle", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectiontitle"), "required", "")
|
hb.WriteElementOpen("input", "type", "text", "name", "sectiontitle", "placeholder", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "sectiontitle"), "required", "")
|
||||||
// Create button
|
// Create button
|
||||||
hb.writeElementOpen("div")
|
hb.WriteElementOpen("div")
|
||||||
hb.writeElementOpen(
|
hb.WriteElementOpen(
|
||||||
"input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "create"),
|
"input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "create"),
|
||||||
"formaction", rd.Blog.getRelativePath(settingsPath+settingsCreateSectionPath),
|
"formaction", rd.Blog.getRelativePath(settingsPath+settingsCreateSectionPath),
|
||||||
)
|
)
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
hb.writeElementClose("form")
|
hb.WriteElementClose("form")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) renderCollapsibleBooleanSetting(hb *htmlBuilder, rd *renderData, path, title, description, name string, value bool) {
|
func (a *goBlog) renderCollapsibleBooleanSetting(hb *htmlbuilder.HtmlBuilder, rd *renderData, path, title, description, name string, value bool) {
|
||||||
hb.writeElementOpen("details")
|
hb.WriteElementOpen("details")
|
||||||
|
|
||||||
hb.writeElementOpen("summary")
|
hb.WriteElementOpen("summary")
|
||||||
hb.writeElementOpen("h3")
|
hb.WriteElementOpen("h3")
|
||||||
hb.writeEscaped(title)
|
hb.WriteEscaped(title)
|
||||||
hb.writeElementClose("h3")
|
hb.WriteElementClose("h3")
|
||||||
hb.writeElementClose("summary")
|
hb.WriteElementClose("summary")
|
||||||
|
|
||||||
hb.writeElementOpen("form", "class", "fw p", "method", "post")
|
hb.WriteElementOpen("form", "class", "fw p", "method", "post")
|
||||||
|
|
||||||
hb.writeElementOpen("input", "type", "checkbox", "name", name, "id", "cb-"+name, lo.If(value, "checked").Else(""), "")
|
hb.WriteElementOpen("input", "type", "checkbox", "name", name, "id", "cb-"+name, lo.If(value, "checked").Else(""), "")
|
||||||
hb.writeElementOpen("label", "for", "cb-"+name)
|
hb.WriteElementOpen("label", "for", "cb-"+name)
|
||||||
hb.writeEscaped(description)
|
hb.WriteEscaped(description)
|
||||||
hb.writeElementClose("label")
|
hb.WriteElementClose("label")
|
||||||
|
|
||||||
hb.writeElementOpen("div", "class", "p")
|
hb.WriteElementOpen("div", "class", "p")
|
||||||
hb.writeElementOpen(
|
hb.WriteElementOpen(
|
||||||
"input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update"), "formaction", path,
|
"input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update"), "formaction", path,
|
||||||
)
|
)
|
||||||
hb.writeElementClose("div")
|
hb.WriteElementClose("div")
|
||||||
|
|
||||||
hb.writeElementClose("form")
|
hb.WriteElementClose("form")
|
||||||
|
|
||||||
hb.writeElementClose("details")
|
hb.WriteElementClose("details")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,72 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
textTemplate "text/template"
|
|
||||||
)
|
|
||||||
|
|
||||||
type htmlBuilder struct {
|
|
||||||
w io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHtmlBuilder(w io.Writer) *htmlBuilder {
|
|
||||||
return &htmlBuilder{
|
|
||||||
w: w,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *htmlBuilder) getWriter() io.Writer {
|
|
||||||
return h.w
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *htmlBuilder) Write(p []byte) (int, error) {
|
|
||||||
return h.getWriter().Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *htmlBuilder) WriteString(s string) (int, error) {
|
|
||||||
return io.WriteString(h.getWriter(), s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *htmlBuilder) write(s string) {
|
|
||||||
_, _ = h.WriteString(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *htmlBuilder) writeEscaped(s string) {
|
|
||||||
textTemplate.HTMLEscape(h, []byte(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *htmlBuilder) writeAttribute(attr string, val any) {
|
|
||||||
h.write(` `)
|
|
||||||
h.write(attr)
|
|
||||||
h.write(`=`)
|
|
||||||
if valStr, ok := val.(string); ok {
|
|
||||||
h.write(`"`)
|
|
||||||
h.writeEscaped(valStr)
|
|
||||||
h.write(`"`)
|
|
||||||
} else {
|
|
||||||
h.writeEscaped(fmt.Sprint(val))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *htmlBuilder) writeElementOpen(tag string, attrs ...any) {
|
|
||||||
h.write(`<`)
|
|
||||||
h.write(tag)
|
|
||||||
for i := 0; i < len(attrs); i += 2 {
|
|
||||||
if i+2 > len(attrs) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
attrStr, ok := attrs[i].(string)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
h.writeAttribute(attrStr, attrs[i+1])
|
|
||||||
}
|
|
||||||
h.write(`>`)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *htmlBuilder) writeElementClose(tag string) {
|
|
||||||
h.write(`</`)
|
|
||||||
h.write(tag)
|
|
||||||
h.write(`>`)
|
|
||||||
}
|
|
13
ui_test.go
13
ui_test.go
|
@ -2,7 +2,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -11,11 +10,9 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.goblog.app/app/pkgs/bufferpool"
|
"go.goblog.app/app/pkgs/bufferpool"
|
||||||
|
"go.goblog.app/app/pkgs/htmlbuilder"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ io.Writer = &htmlBuilder{}
|
|
||||||
var _ io.StringWriter = &htmlBuilder{}
|
|
||||||
|
|
||||||
func Test_renderPostTax(t *testing.T) {
|
func Test_renderPostTax(t *testing.T) {
|
||||||
app := &goBlog{
|
app := &goBlog{
|
||||||
cfg: createDefaultTestConfig(t),
|
cfg: createDefaultTestConfig(t),
|
||||||
|
@ -33,7 +30,7 @@ func Test_renderPostTax(t *testing.T) {
|
||||||
buf := bufferpool.Get()
|
buf := bufferpool.Get()
|
||||||
defer bufferpool.Put(buf)
|
defer bufferpool.Put(buf)
|
||||||
|
|
||||||
hb := newHtmlBuilder(buf)
|
hb := htmlbuilder.NewHtmlBuilder(buf)
|
||||||
|
|
||||||
app.renderPostTax(hb, p, app.cfg.Blogs["default"])
|
app.renderPostTax(hb, p, app.cfg.Blogs["default"])
|
||||||
|
|
||||||
|
@ -56,7 +53,7 @@ func Test_renderOldContentWarning(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
hb := newHtmlBuilder(buf)
|
hb := htmlbuilder.NewHtmlBuilder(buf)
|
||||||
|
|
||||||
app.renderOldContentWarning(hb, p, app.cfg.Blogs["default"])
|
app.renderOldContentWarning(hb, p, app.cfg.Blogs["default"])
|
||||||
res := buf.String()
|
res := buf.String()
|
||||||
|
@ -122,7 +119,7 @@ func Test_renderInteractions(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
hb := newHtmlBuilder(buf)
|
hb := htmlbuilder.NewHtmlBuilder(buf)
|
||||||
|
|
||||||
app.renderInteractions(hb, &renderData{
|
app.renderInteractions(hb, &renderData{
|
||||||
Blog: app.cfg.Blogs["default"],
|
Blog: app.cfg.Blogs["default"],
|
||||||
|
@ -149,7 +146,7 @@ func Test_renderAuthor(t *testing.T) {
|
||||||
_ = app.initConfig(false)
|
_ = app.initConfig(false)
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
hb := newHtmlBuilder(buf)
|
hb := htmlbuilder.NewHtmlBuilder(buf)
|
||||||
|
|
||||||
app.renderAuthor(hb)
|
app.renderAuthor(hb)
|
||||||
res := buf.String()
|
res := buf.String()
|
||||||
|
|
Loading…
Reference in New Issue