feat(templates): add HTMLComponent interface and first-class pongo2 engine
Decouple TemplateFunc from templ.Component by introducing a generic HTMLComponent interface that both templ and pongo2 satisfy via Go structural typing. Add a complete pongo2 rendering engine in templates/pongo/ with page templates, block templates (with BlockContext injection and icon processing), template overrides, and email wrappers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7f4bce79c9
commit
32c6528162
9
CLAUDE.md
Normal file
9
CLAUDE.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Core SDK
|
||||||
|
|
||||||
|
Go module `git.dev.alexdunmow.com/block/core`. Defines plugin interfaces, template engine, block registry, and shared types for the BlockNinja CMS.
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
- **NEVER use `replace` directives in go.mod** — not in this repo, not in any consumer. All module resolution goes through the Gitea module proxy. If you need to test local changes, tag and push a version.
|
||||||
|
- Plugins import from core only — never from the CMS (`blockninja/backend`) or orchestrator.
|
||||||
|
- All consumers are in-house — no backwards compatibility shims needed. Just change the API and update consumers.
|
||||||
2
go.mod
2
go.mod
@ -6,6 +6,7 @@ require (
|
|||||||
connectrpc.com/connect v1.20.0
|
connectrpc.com/connect v1.20.0
|
||||||
github.com/BurntSushi/toml v1.6.0
|
github.com/BurntSushi/toml v1.6.0
|
||||||
github.com/a-h/templ v0.3.1001
|
github.com/a-h/templ v0.3.1001
|
||||||
|
github.com/flosch/pongo2/v6 v6.1.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgx/v5 v5.9.2
|
github.com/jackc/pgx/v5 v5.9.2
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
@ -17,6 +18,7 @@ require (
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.15.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.9 // indirect
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
golang.org/x/text v0.36.0 // indirect
|
golang.org/x/text v0.36.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
10
go.sum
10
go.sum
@ -8,6 +8,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/flosch/pongo2/v6 v6.1.0 h1:A/NJbrQJJD2B2mbpw3DRFwBYG0xpCr3vwFlEr46y1HQ=
|
||||||
|
github.com/flosch/pongo2/v6 v6.1.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@ -22,8 +24,14 @@ github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
|||||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
|
||||||
|
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
@ -44,6 +52,8 @@ golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
|||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
9
templates/pongo/base.html
Normal file
9
templates/pongo/base.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="{{ theme_mode }}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="{% block body_class %}bg-background text-foreground antialiased min-h-screen flex flex-col{% endblock %}">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
101
templates/pongo/context.go
Normal file
101
templates/pongo/context.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package pongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/flosch/pongo2/v6"
|
||||||
|
|
||||||
|
"git.dev.alexdunmow.com/block/core/blocks"
|
||||||
|
"git.dev.alexdunmow.com/block/core/templates/bn"
|
||||||
|
)
|
||||||
|
|
||||||
|
// buildPageContext builds a pongo2.Context with pre-rendered head/body HTML
|
||||||
|
// and all page-level variables from the standard doc map.
|
||||||
|
func (e *Engine) buildPageContext(ctx context.Context, doc map[string]any) pongo2.Context {
|
||||||
|
title := "Untitled"
|
||||||
|
if t, ok := doc["title"].(string); ok && t != "" {
|
||||||
|
title = t
|
||||||
|
}
|
||||||
|
|
||||||
|
slots := make(map[string]string)
|
||||||
|
if s, ok := doc["slots"].(map[string]string); ok {
|
||||||
|
slots = s
|
||||||
|
}
|
||||||
|
|
||||||
|
themeMode := "dark"
|
||||||
|
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
|
||||||
|
themeMode = tm
|
||||||
|
}
|
||||||
|
|
||||||
|
themeCSS := ""
|
||||||
|
if tc, ok := doc["theme_css"].(string); ok {
|
||||||
|
themeCSS = tc
|
||||||
|
}
|
||||||
|
|
||||||
|
structuredData := ""
|
||||||
|
if sd, ok := doc["structured_data"].(string); ok {
|
||||||
|
structuredData = sd
|
||||||
|
}
|
||||||
|
|
||||||
|
cssHash := ""
|
||||||
|
if ch, ok := doc["css_hash"].(string); ok {
|
||||||
|
cssHash = ch
|
||||||
|
}
|
||||||
|
|
||||||
|
pageviewNonce := ""
|
||||||
|
if pn, ok := doc["pageview_nonce"].(string); ok {
|
||||||
|
pageviewNonce = pn
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := bn.ParseSiteSettings(doc)
|
||||||
|
pageMeta := bn.ParsePageMeta(doc)
|
||||||
|
engagementConfig := bn.ParseEngagementConfig(doc)
|
||||||
|
|
||||||
|
headHTML := renderComponent(ctx, bn.Head(bn.HeadData{
|
||||||
|
Title: title,
|
||||||
|
Settings: settings,
|
||||||
|
PageMeta: pageMeta,
|
||||||
|
ThemeMode: themeMode,
|
||||||
|
ThemeCSS: themeCSS,
|
||||||
|
PluginStyles: e.stylePaths,
|
||||||
|
StructuredData: structuredData,
|
||||||
|
CSSHash: cssHash,
|
||||||
|
PageviewNonce: pageviewNonce,
|
||||||
|
EngagementConfig: engagementConfig,
|
||||||
|
}))
|
||||||
|
bodyEndHTML := renderComponent(ctx, bn.BodyEnd(settings))
|
||||||
|
bannerHTML := renderComponent(ctx, bn.AdminBypassBanner(settings))
|
||||||
|
|
||||||
|
slotsAny := make(map[string]any, len(slots))
|
||||||
|
for k, v := range slots {
|
||||||
|
slotsAny[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return pongo2.Context{
|
||||||
|
"head_html": headHTML,
|
||||||
|
"body_end_html": bodyEndHTML,
|
||||||
|
"admin_banner_html": bannerHTML,
|
||||||
|
|
||||||
|
"title": title,
|
||||||
|
"slots": slotsAny,
|
||||||
|
"theme_mode": themeMode,
|
||||||
|
"theme_css": themeCSS,
|
||||||
|
"css_hash": cssHash,
|
||||||
|
|
||||||
|
"site_settings": settings,
|
||||||
|
"page_meta": pageMeta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildBlockContext builds a pongo2.Context for block rendering.
|
||||||
|
// Content fields are available directly; request context is under "ctx".
|
||||||
|
func buildBlockContext(ctx context.Context, content map[string]any) pongo2.Context {
|
||||||
|
pongoCtx := make(pongo2.Context, len(content)+1)
|
||||||
|
for k, v := range content {
|
||||||
|
pongoCtx[k] = v
|
||||||
|
}
|
||||||
|
if bc := blocks.GetBlockContext(ctx); bc != nil {
|
||||||
|
pongoCtx["ctx"] = bc.ToMap()
|
||||||
|
}
|
||||||
|
return pongoCtx
|
||||||
|
}
|
||||||
6
templates/pongo/embed.go
Normal file
6
templates/pongo/embed.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package pongo
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed base.html
|
||||||
|
var baseFS embed.FS
|
||||||
172
templates/pongo/engine.go
Normal file
172
templates/pongo/engine.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package pongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
|
"github.com/flosch/pongo2/v6"
|
||||||
|
|
||||||
|
"git.dev.alexdunmow.com/block/core/blocks"
|
||||||
|
"git.dev.alexdunmow.com/block/core/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Engine provides first-class pongo2 template support for BlockNinja plugins.
|
||||||
|
// Plugins create an Engine with their embedded template FS, then call
|
||||||
|
// MustPageTemplate / MustBlockTemplate to get functions compatible with
|
||||||
|
// the template and block registries.
|
||||||
|
type Engine struct {
|
||||||
|
set *pongo2.TemplateSet
|
||||||
|
stylePaths []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEngine creates a pongo2 Engine.
|
||||||
|
// pluginFS should contain the plugin's .html templates.
|
||||||
|
// stylePaths are CSS URLs included in the page <head> via bn.Head.
|
||||||
|
func NewEngine(pluginFS fs.FS, stylePaths ...string) *Engine {
|
||||||
|
loader := &multiLoader{
|
||||||
|
loaders: []pongo2.TemplateLoader{
|
||||||
|
&fsLoader{fsys: pluginFS},
|
||||||
|
&fsLoader{fsys: baseFS},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
set := pongo2.NewSet("plugin", loader)
|
||||||
|
return &Engine{set: set, stylePaths: stylePaths}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pongoComponent wraps a pongo2 template execution as an HTMLComponent.
|
||||||
|
type pongoComponent struct {
|
||||||
|
tpl *pongo2.Template
|
||||||
|
ctx pongo2.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pongoComponent) Render(ctx context.Context, w io.Writer) error {
|
||||||
|
return c.tpl.ExecuteWriter(c.ctx, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustPageTemplate parses a pongo2 page template and returns a TemplateFunc.
|
||||||
|
// Panics on parse error.
|
||||||
|
//
|
||||||
|
// Available context variables in page templates:
|
||||||
|
//
|
||||||
|
// {{ head_html|safe }} — full <head> element
|
||||||
|
// {{ body_end_html|safe }} — body-end scripts and admin toolbar
|
||||||
|
// {{ admin_banner_html|safe }} — maintenance/coming-soon admin banner
|
||||||
|
// {{ title }} — page title
|
||||||
|
// {{ slots.header|safe }} — rendered slot HTML
|
||||||
|
// {{ theme_mode }} — "light", "dark", or "system"
|
||||||
|
// {{ theme_css }} — raw CSS custom properties
|
||||||
|
// {{ css_hash }} — cache-busting hash
|
||||||
|
// {{ site_settings }} — bn.SiteSettingsData struct
|
||||||
|
// {{ page_meta }} — bn.PageMeta struct
|
||||||
|
func (e *Engine) MustPageTemplate(name string) templates.TemplateFunc {
|
||||||
|
tpl := pongo2.Must(e.set.FromFile(name))
|
||||||
|
return func(ctx context.Context, doc map[string]any) templates.HTMLComponent {
|
||||||
|
pongoCtx := e.buildPageContext(ctx, doc)
|
||||||
|
return &pongoComponent{tpl: tpl, ctx: pongoCtx}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustBlockTemplate parses a pongo2 block template and returns a BlockFunc.
|
||||||
|
// Panics on parse error.
|
||||||
|
//
|
||||||
|
// The content map fields are available directly as template variables.
|
||||||
|
// Request context is available under {{ ctx.url }}, {{ ctx.isEditor }}, etc.
|
||||||
|
func (e *Engine) MustBlockTemplate(name string) blocks.BlockFunc {
|
||||||
|
tpl := pongo2.Must(e.set.FromFile(name))
|
||||||
|
return func(ctx context.Context, content map[string]any) string {
|
||||||
|
pongoCtx := buildBlockContext(ctx, content)
|
||||||
|
out, err := tpl.Execute(pongoCtx)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return blocks.ProcessIcons(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustBlockTemplateWithDefaults is like MustBlockTemplate but merges default
|
||||||
|
// values before rendering. Content keys override defaults.
|
||||||
|
func (e *Engine) MustBlockTemplateWithDefaults(name string, defaults map[string]any) blocks.BlockFunc {
|
||||||
|
tpl := pongo2.Must(e.set.FromFile(name))
|
||||||
|
return func(ctx context.Context, content map[string]any) string {
|
||||||
|
merged := make(map[string]any, len(defaults)+len(content))
|
||||||
|
for k, v := range defaults {
|
||||||
|
merged[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range content {
|
||||||
|
merged[k] = v
|
||||||
|
}
|
||||||
|
pongoCtx := buildBlockContext(ctx, merged)
|
||||||
|
out, err := tpl.Execute(pongoCtx)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return blocks.ProcessIcons(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustTemplateOverride parses a pongo2 template for use as a block template
|
||||||
|
// override (e.g., custom heading/text styling). Same as MustBlockTemplate
|
||||||
|
// but named distinctly for clarity at the call site.
|
||||||
|
func (e *Engine) MustTemplateOverride(name string) blocks.BlockFunc {
|
||||||
|
return e.MustBlockTemplate(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustEmailWrapper parses a pongo2 email template and returns an
|
||||||
|
// EmailWrapperFunc. Panics on parse error.
|
||||||
|
//
|
||||||
|
// Available context variables:
|
||||||
|
//
|
||||||
|
// {{ body|safe }} — the email body HTML
|
||||||
|
// {{ site_name }} — site name
|
||||||
|
// {{ site_url }} — site URL
|
||||||
|
// {{ logo_url }} — site logo URL
|
||||||
|
// {{ unsubscribe_url }} — unsubscribe link (may be empty)
|
||||||
|
// {{ preview_text }} — email preview/preheader text
|
||||||
|
// {{ colors.primary }} — primary hex color
|
||||||
|
// {{ colors.secondary }} — secondary hex color
|
||||||
|
// {{ colors.background }} — background hex color
|
||||||
|
// {{ colors.foreground }} — foreground hex color
|
||||||
|
// {{ colors.border }} — border hex color
|
||||||
|
// {{ colors.muted }} — muted hex color
|
||||||
|
// (and all other EmailColors fields as lowercase keys)
|
||||||
|
func (e *Engine) MustEmailWrapper(name string) templates.EmailWrapperFunc {
|
||||||
|
tpl := pongo2.Must(e.set.FromFile(name))
|
||||||
|
return func(body string, ctx templates.EmailContext) string {
|
||||||
|
pongoCtx := pongo2.Context{
|
||||||
|
"body": body,
|
||||||
|
"site_name": ctx.SiteSettings.SiteName,
|
||||||
|
"site_url": ctx.SiteSettings.SiteURL,
|
||||||
|
"logo_url": ctx.SiteSettings.LogoURL,
|
||||||
|
"support_email": ctx.SiteSettings.SupportEmail,
|
||||||
|
"unsubscribe_url": ctx.UnsubscribeURL,
|
||||||
|
"preview_text": ctx.PreviewText,
|
||||||
|
"colors": map[string]any{
|
||||||
|
"primary": ctx.Colors.Primary,
|
||||||
|
"primaryForeground": ctx.Colors.PrimaryForeground,
|
||||||
|
"secondary": ctx.Colors.Secondary,
|
||||||
|
"secondaryForeground": ctx.Colors.SecondaryForeground,
|
||||||
|
"background": ctx.Colors.Background,
|
||||||
|
"foreground": ctx.Colors.Foreground,
|
||||||
|
"muted": ctx.Colors.Muted,
|
||||||
|
"mutedForeground": ctx.Colors.MutedForeground,
|
||||||
|
"border": ctx.Colors.Border,
|
||||||
|
"card": ctx.Colors.Card,
|
||||||
|
"cardForeground": ctx.Colors.CardForeground,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
out, err := tpl.Execute(pongoCtx)
|
||||||
|
if err != nil {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderComponent renders any HTMLComponent to a string.
|
||||||
|
func renderComponent(ctx context.Context, c templates.HTMLComponent) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = c.Render(ctx, &buf)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
43
templates/pongo/loader.go
Normal file
43
templates/pongo/loader.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package pongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
|
"github.com/flosch/pongo2/v6"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fsLoader adapts an fs.FS to pongo2's TemplateLoader interface.
|
||||||
|
type fsLoader struct {
|
||||||
|
fsys fs.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *fsLoader) Abs(base, name string) string {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *fsLoader) Get(path string) (io.Reader, error) {
|
||||||
|
return l.fsys.Open(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiLoader chains multiple loaders, returning the first hit.
|
||||||
|
// Plugin templates can {% extends "base.html" %} where base.html
|
||||||
|
// lives in the core embedded FS rather than the plugin FS.
|
||||||
|
type multiLoader struct {
|
||||||
|
loaders []pongo2.TemplateLoader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *multiLoader) Abs(base, name string) string {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *multiLoader) Get(path string) (io.Reader, error) {
|
||||||
|
for _, loader := range l.loaders {
|
||||||
|
r, err := loader.Get(path)
|
||||||
|
if err == nil {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("pongo: template %q not found in any loader", path)
|
||||||
|
}
|
||||||
@ -2,12 +2,17 @@ package templates
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"github.com/a-h/templ"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// HTMLComponent is the generic interface for rendered template output.
|
||||||
|
// Both templ.Component and pongo2 engine output satisfy this interface.
|
||||||
|
type HTMLComponent interface {
|
||||||
|
Render(ctx context.Context, w io.Writer) error
|
||||||
|
}
|
||||||
|
|
||||||
// TemplateFunc is the signature for template functions loaded from plugins.
|
// TemplateFunc is the signature for template functions loaded from plugins.
|
||||||
type TemplateFunc func(ctx context.Context, doc map[string]any) templ.Component
|
type TemplateFunc func(ctx context.Context, doc map[string]any) HTMLComponent
|
||||||
|
|
||||||
// PageTemplateMeta provides metadata about a page template within a system template.
|
// PageTemplateMeta provides metadata about a page template within a system template.
|
||||||
type PageTemplateMeta struct {
|
type PageTemplateMeta struct {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user