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 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 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() }