- Define PageContext, PostContext, AuthorContext, CategoryContext, MasterPageContext structs for typed context passing - Define EmbedResolver interface - Make GetQueries generic: GetQueries[T](ctx) (T, bool) - Fix Content.BlockNoteToHTML to take map[string]any, not any Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
449 lines
12 KiB
Go
449 lines
12 KiB
Go
package blocks
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// --- Typed context structs ---
|
|
|
|
// PageContext provides page information to blocks via context.
|
|
// The CMS populates this from its internal db.Page type.
|
|
type PageContext struct {
|
|
ID uuid.UUID
|
|
Slug string
|
|
Title string
|
|
PostType string // "page", "post", "master", "system"
|
|
Status string // "published", "draft", "scheduled"
|
|
}
|
|
|
|
// PostContext provides blog post information to blocks via context.
|
|
type PostContext struct {
|
|
ID uuid.UUID
|
|
Slug string
|
|
Title string
|
|
Excerpt string
|
|
FeaturedImageURL string
|
|
AuthorID uuid.UUID
|
|
PublishedAt time.Time
|
|
ReadingTime int
|
|
IsFeatured bool
|
|
}
|
|
|
|
// AuthorContext provides author information to blocks via context.
|
|
type AuthorContext struct {
|
|
ID uuid.UUID
|
|
Name string
|
|
Slug string
|
|
Bio string
|
|
AvatarURL string
|
|
}
|
|
|
|
// CategoryContext provides category information to blocks via context.
|
|
type CategoryContext struct {
|
|
ID uuid.UUID
|
|
Name string
|
|
Slug string
|
|
}
|
|
|
|
// MasterPageContext provides master page information during rendering.
|
|
type MasterPageContext struct {
|
|
ID uuid.UUID
|
|
Slug string
|
|
Title string
|
|
}
|
|
|
|
// EmbedResolver resolves embedded component blocks within blog content.
|
|
type EmbedResolver interface {
|
|
RenderEmbed(ctx context.Context, blockID uuid.UUID, dataSource, layout string) string
|
|
}
|
|
|
|
// --- Template key ---
|
|
|
|
type templateKeyContextKey struct{}
|
|
|
|
func WithTemplateKey(ctx context.Context, templateKey string) context.Context {
|
|
return context.WithValue(ctx, templateKeyContextKey{}, templateKey)
|
|
}
|
|
|
|
func GetTemplateKey(ctx context.Context) string {
|
|
if v, ok := ctx.Value(templateKeyContextKey{}).(string); ok {
|
|
return v
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// --- Queries (generic) ---
|
|
|
|
type queriesContextKey struct{}
|
|
|
|
// WithQueries stores a queries value in context. Use with GetQueries[T].
|
|
func WithQueries[T any](ctx context.Context, queries T) context.Context {
|
|
return context.WithValue(ctx, queriesContextKey{}, queries)
|
|
}
|
|
|
|
// GetQueries retrieves a typed queries value from context.
|
|
// Usage: queries, ok := blocks.GetQueries[*db.Queries](ctx)
|
|
func GetQueries[T any](ctx context.Context) (T, bool) {
|
|
val, ok := ctx.Value(queriesContextKey{}).(T)
|
|
return val, ok
|
|
}
|
|
|
|
// --- Current page ---
|
|
|
|
type currentPageContextKey struct{}
|
|
|
|
func WithCurrentPage(ctx context.Context, page *PageContext) context.Context {
|
|
return context.WithValue(ctx, currentPageContextKey{}, page)
|
|
}
|
|
|
|
func GetCurrentPage(ctx context.Context) *PageContext {
|
|
if v, ok := ctx.Value(currentPageContextKey{}).(*PageContext); ok {
|
|
return v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- Current blog post ---
|
|
|
|
type currentBlogPostContextKey struct{}
|
|
|
|
func WithCurrentBlogPost(ctx context.Context, post *PostContext) context.Context {
|
|
return context.WithValue(ctx, currentBlogPostContextKey{}, post)
|
|
}
|
|
|
|
func GetCurrentBlogPost(ctx context.Context) *PostContext {
|
|
if v, ok := ctx.Value(currentBlogPostContextKey{}).(*PostContext); ok {
|
|
return v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- Current author ---
|
|
|
|
type currentAuthorContextKey struct{}
|
|
|
|
func WithCurrentAuthor(ctx context.Context, author *AuthorContext) context.Context {
|
|
return context.WithValue(ctx, currentAuthorContextKey{}, author)
|
|
}
|
|
|
|
func GetCurrentAuthor(ctx context.Context) *AuthorContext {
|
|
if v, ok := ctx.Value(currentAuthorContextKey{}).(*AuthorContext); ok {
|
|
return v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- Current category ---
|
|
|
|
type currentCategoryContextKey struct{}
|
|
|
|
func WithCurrentCategory(ctx context.Context, category *CategoryContext) context.Context {
|
|
return context.WithValue(ctx, currentCategoryContextKey{}, category)
|
|
}
|
|
|
|
func GetCurrentCategory(ctx context.Context) *CategoryContext {
|
|
if v, ok := ctx.Value(currentCategoryContextKey{}).(*CategoryContext); ok {
|
|
return v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- Requested path (for 404 pages) ---
|
|
|
|
type requestedPathContextKey struct{}
|
|
|
|
func WithRequestedPath(ctx context.Context, path string) context.Context {
|
|
return context.WithValue(ctx, requestedPathContextKey{}, path)
|
|
}
|
|
|
|
func GetRequestedPath(ctx context.Context) string {
|
|
if v, ok := ctx.Value(requestedPathContextKey{}).(string); ok {
|
|
return v
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// --- Injected slots (master page rendering) ---
|
|
|
|
type injectedSlotsContextKey struct{}
|
|
|
|
func WithInjectedSlots(ctx context.Context, slots map[string]string) context.Context {
|
|
return context.WithValue(ctx, injectedSlotsContextKey{}, slots)
|
|
}
|
|
|
|
func GetInjectedSlotContent(ctx context.Context, slotName string) string {
|
|
if slots, ok := ctx.Value(injectedSlotsContextKey{}).(map[string]string); ok {
|
|
return slots[slotName]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func GetInjectedSlots(ctx context.Context) map[string]string {
|
|
if slots, ok := ctx.Value(injectedSlotsContextKey{}).(map[string]string); ok {
|
|
return slots
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- Master page ---
|
|
|
|
type masterPageContextKey struct{}
|
|
|
|
func WithMasterPage(ctx context.Context, masterPage *MasterPageContext) context.Context {
|
|
return context.WithValue(ctx, masterPageContextKey{}, masterPage)
|
|
}
|
|
|
|
func GetMasterPage(ctx context.Context) *MasterPageContext {
|
|
if v, ok := ctx.Value(masterPageContextKey{}).(*MasterPageContext); ok {
|
|
return v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func IsMasterPageContext(ctx context.Context) bool {
|
|
return ctx.Value(masterPageContextKey{}) != nil
|
|
}
|
|
|
|
// --- HTTP request ---
|
|
|
|
type requestContextKey struct{}
|
|
|
|
func WithRequest(ctx context.Context, r *http.Request) context.Context {
|
|
return context.WithValue(ctx, requestContextKey{}, r)
|
|
}
|
|
|
|
func GetRequest(ctx context.Context) *http.Request {
|
|
if r, ok := ctx.Value(requestContextKey{}).(*http.Request); ok {
|
|
return r
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- Editor mode ---
|
|
|
|
type isEditorContextKey struct{}
|
|
|
|
func WithIsEditor(ctx context.Context, isEditor bool) context.Context {
|
|
return context.WithValue(ctx, isEditorContextKey{}, isEditor)
|
|
}
|
|
|
|
func IsEditor(ctx context.Context) bool {
|
|
if v, ok := ctx.Value(isEditorContextKey{}).(bool); ok {
|
|
return v
|
|
}
|
|
return false
|
|
}
|
|
|
|
// --- Expected slots ---
|
|
|
|
type expectedSlotsContextKey struct{}
|
|
|
|
func WithExpectedSlots(ctx context.Context, slots []string) context.Context {
|
|
return context.WithValue(ctx, expectedSlotsContextKey{}, slots)
|
|
}
|
|
|
|
func GetExpectedSlots(ctx context.Context) []string {
|
|
if v, ok := ctx.Value(expectedSlotsContextKey{}).([]string); ok {
|
|
return v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- Block ID ---
|
|
|
|
type blockIDContextKey struct{}
|
|
|
|
func WithBlockID(ctx context.Context, id uuid.UUID) context.Context {
|
|
return context.WithValue(ctx, blockIDContextKey{}, id)
|
|
}
|
|
|
|
func GetBlockID(ctx context.Context) uuid.UUID {
|
|
if v, ok := ctx.Value(blockIDContextKey{}).(uuid.UUID); ok {
|
|
return v
|
|
}
|
|
return uuid.Nil
|
|
}
|
|
|
|
// --- Current page ID ---
|
|
|
|
type currentPageIDContextKey struct{}
|
|
|
|
func WithCurrentPageID(ctx context.Context, id uuid.UUID) context.Context {
|
|
return context.WithValue(ctx, currentPageIDContextKey{}, id)
|
|
}
|
|
|
|
func GetCurrentPageID(ctx context.Context) uuid.UUID {
|
|
if v, ok := ctx.Value(currentPageIDContextKey{}).(uuid.UUID); ok {
|
|
return v
|
|
}
|
|
return uuid.Nil
|
|
}
|
|
|
|
// --- Slot renderer ---
|
|
|
|
type slotRendererContextKey struct{}
|
|
|
|
func WithSlotRenderer(ctx context.Context, r SlotRenderer) context.Context {
|
|
return context.WithValue(ctx, slotRendererContextKey{}, r)
|
|
}
|
|
|
|
func GetSlotRenderer(ctx context.Context) SlotRenderer {
|
|
if r, ok := ctx.Value(slotRendererContextKey{}).(SlotRenderer); ok {
|
|
return r
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- Human proof banner ---
|
|
|
|
type humanProofBannerContextKey struct{}
|
|
|
|
func WithHumanProofBanner(ctx context.Context, data *HumanProofBannerData) context.Context {
|
|
return context.WithValue(ctx, humanProofBannerContextKey{}, data)
|
|
}
|
|
|
|
func GetHumanProofBanner(ctx context.Context) *HumanProofBannerData {
|
|
if v, ok := ctx.Value(humanProofBannerContextKey{}).(*HumanProofBannerData); ok {
|
|
return v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- Embed resolver ---
|
|
|
|
type embedResolverContextKey struct{}
|
|
|
|
func WithEmbedResolver(ctx context.Context, resolver EmbedResolver) context.Context {
|
|
return context.WithValue(ctx, embedResolverContextKey{}, resolver)
|
|
}
|
|
|
|
func GetEmbedResolver(ctx context.Context) EmbedResolver {
|
|
if r, ok := ctx.Value(embedResolverContextKey{}).(EmbedResolver); ok {
|
|
return r
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- RenderSlot ---
|
|
|
|
// RenderSlot renders children for the current container block.
|
|
func RenderSlot(ctx context.Context) string {
|
|
blockID := GetBlockID(ctx)
|
|
if blockID == uuid.Nil {
|
|
return ""
|
|
}
|
|
renderer := GetSlotRenderer(ctx)
|
|
if renderer == nil {
|
|
return ""
|
|
}
|
|
return renderer.RenderContainerSlot(ctx, blockID)
|
|
}
|
|
|
|
// --- BlockContext (pongo2 template data) ---
|
|
|
|
// BlockContext provides contextual information available to all block templates.
|
|
type BlockContext struct {
|
|
URL string `pongo2:"url"`
|
|
Path string `pongo2:"path"`
|
|
Slug string `pongo2:"slug"`
|
|
PageID string `pongo2:"pageId"`
|
|
PageTitle string `pongo2:"pageTitle"`
|
|
TemplateKey string `pongo2:"templateKey"`
|
|
IsEditor bool `pongo2:"isEditor"`
|
|
Timestamp int64 `pongo2:"timestamp"`
|
|
IsLoggedIn bool `pongo2:"isLoggedIn"`
|
|
UserID string `pongo2:"userId"`
|
|
UserEmail string `pongo2:"userEmail"`
|
|
UserRole string `pongo2:"userRole"`
|
|
Method string `pongo2:"method"`
|
|
Host string `pongo2:"host"`
|
|
Query map[string]string `pongo2:"query"`
|
|
Referrer string `pongo2:"referrer"`
|
|
UserAgent string `pongo2:"userAgent"`
|
|
IP string `pongo2:"ip"`
|
|
Cookies map[string]string `pongo2:"cookies"`
|
|
Headers map[string]string `pongo2:"headers"`
|
|
Country string `pongo2:"country"`
|
|
City string `pongo2:"city"`
|
|
Timezone string `pongo2:"timezone"`
|
|
Now time.Time `pongo2:"now"`
|
|
CurrentAuthor map[string]any `pongo2:"currentAuthor"`
|
|
CurrentPost map[string]any `pongo2:"currentPost"`
|
|
CurrentCategory map[string]any `pongo2:"currentCategory"`
|
|
Site map[string]any `pongo2:"site"`
|
|
IsPublicLoggedIn bool `pongo2:"isPublicLoggedIn"`
|
|
PublicUserID string `pongo2:"publicUserId"`
|
|
PublicUsername string `pongo2:"publicUsername"`
|
|
PublicDisplayName string `pongo2:"publicDisplayName"`
|
|
PublicEmailVerified bool `pongo2:"publicEmailVerified"`
|
|
DetailRowID string `pongo2:"detailRowId"`
|
|
DetailTableID string `pongo2:"detailTableId"`
|
|
DetailRowData map[string]any `pongo2:"detailRowData"`
|
|
BlogIndexURL string `pongo2:"blogIndexUrl"`
|
|
CategoryPageURL string `pongo2:"categoryPageUrl"`
|
|
}
|
|
|
|
type blockContextKey struct{}
|
|
|
|
func WithBlockContext(ctx context.Context, blockCtx *BlockContext) context.Context {
|
|
return context.WithValue(ctx, blockContextKey{}, blockCtx)
|
|
}
|
|
|
|
func GetBlockContext(ctx context.Context) *BlockContext {
|
|
if bc, ok := ctx.Value(blockContextKey{}).(*BlockContext); ok {
|
|
return bc
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ToMap converts the BlockContext to a map for pongo2 template injection.
|
|
func (bc *BlockContext) ToMap() map[string]any {
|
|
return map[string]any{
|
|
"url": bc.URL, "path": bc.Path, "slug": bc.Slug,
|
|
"pageId": bc.PageID, "pageTitle": bc.PageTitle,
|
|
"templateKey": bc.TemplateKey, "isEditor": bc.IsEditor,
|
|
"timestamp": bc.Timestamp, "now": bc.Now,
|
|
"isLoggedIn": bc.IsLoggedIn, "userId": bc.UserID,
|
|
"userEmail": bc.UserEmail, "userRole": bc.UserRole,
|
|
"method": bc.Method, "host": bc.Host, "query": bc.Query,
|
|
"referrer": bc.Referrer, "userAgent": bc.UserAgent,
|
|
"ip": bc.IP, "cookies": bc.Cookies, "headers": bc.Headers,
|
|
"country": bc.Country, "city": bc.City, "timezone": bc.Timezone,
|
|
"currentAuthor": bc.CurrentAuthor, "currentPost": bc.CurrentPost,
|
|
"currentCategory": bc.CurrentCategory, "site": bc.Site,
|
|
"isPublicLoggedIn": bc.IsPublicLoggedIn,
|
|
"publicUserId": bc.PublicUserID, "publicUsername": bc.PublicUsername,
|
|
"publicDisplayName": bc.PublicDisplayName,
|
|
"publicEmailVerified": bc.PublicEmailVerified,
|
|
"detailRowId": bc.DetailRowID, "detailTableId": bc.DetailTableID,
|
|
"detailRowData": bc.DetailRowData,
|
|
"blogIndexUrl": bc.BlogIndexURL, "categoryPageUrl": bc.CategoryPageURL,
|
|
}
|
|
}
|
|
|
|
// --- Detail row ---
|
|
|
|
// DetailRowInfo holds the resolved data table row for detail pages.
|
|
type DetailRowInfo struct {
|
|
TableID string
|
|
RowID string
|
|
Data map[string]any
|
|
}
|
|
|
|
type detailRowKey struct{}
|
|
|
|
func WithDetailRow(ctx context.Context, tableID, rowID string, data map[string]any) context.Context {
|
|
return context.WithValue(ctx, detailRowKey{}, &DetailRowInfo{TableID: tableID, RowID: rowID, Data: data})
|
|
}
|
|
|
|
func GetDetailRow(ctx context.Context) *DetailRowInfo {
|
|
if v, ok := ctx.Value(detailRowKey{}).(*DetailRowInfo); ok {
|
|
return v
|
|
}
|
|
return nil
|
|
}
|