core/blocks/context.go
Alex Dunmow 79c558a968 fix: replace any with typed context accessors in SDK
- 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>
2026-04-30 22:39:34 +08:00

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
}