pluginsdk/blocks/types.go
Alex Dunmow b6d40ed8ac feat: bootstrap block/pluginsdk — the proto-first plugin SDK (P1)
Bootstrap the plugin-facing SDK module so the fleet migration (P2) becomes a
mechanical block/core/X -> block/pluginsdk/X import rewrite.

- abi/proto/v1: the single source-of-truth ABI proto tree, copied from the cms
  authoring source (cms/backend/abi/proto/v1) with go_package retargeted to
  git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1. Kills the hand-kept
  cms/core proto duplication (audit gap 4).
- abi/v1: generated Go bindings (buf generate); byte-identical to core's abi/v1
  save the embedded go_package path.
- plugin/ (registration + DI surface), plugin/wasmguest/** (transport shim,
  caps stubs, bnwasm db driver, testdata fixtures + golden .pb), and the
  guest-facing type packages: blocks (+builtin/shared/tags), templates
  (+pongo/bn), auth, settings, content, gating, crypto, rbac, video, ai,
  subscriptions, menus, datasources. Internal imports rewritten core ->
  pluginsdk; zero block/core references remain.
- README/AGENTS(+CLAUDE symlink)/Makefile: proto is the contract, Go is one
  binding; no replace directives; templates/bn is a synced copy authored in cms.

Spec: cms docs/superpowers/specs/2026-07-07-proto-first-plugin-sdk-design.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:51:00 +08:00

107 lines
3.3 KiB
Go

package blocks
import (
"context"
"fmt"
"strings"
"github.com/google/uuid"
)
// BlockFunc renders a block given its content data.
type BlockFunc func(ctx context.Context, content map[string]any) string
// BlockCategory represents categories for organizing blocks in the palette.
type BlockCategory string
const (
CategoryContent BlockCategory = "content"
CategoryLayout BlockCategory = "layout"
CategoryNavigation BlockCategory = "navigation"
CategoryBlog BlockCategory = "blog"
CategoryTheme BlockCategory = "theme"
)
// BlockMeta provides metadata about a block type for admin UI and validation.
type BlockMeta struct {
Key string
Title string
Description string
Category BlockCategory
HasInternalSlot bool
Source string
Hidden bool
EditorJS string
}
// BlockNode represents a block with its content and children for rendering.
type BlockNode struct {
ID uuid.UUID
BlockKey string
Title string
Content map[string]any
HtmlContent string
Children []*BlockNode
SortOrder int
}
// SlotRenderer is the interface for rendering container slots.
type SlotRenderer interface {
RenderContainerSlot(ctx context.Context, containerID uuid.UUID) string
}
// HumanProofBannerData holds data for the public human proof banner.
type HumanProofBannerData struct {
ActiveTimeMinutes int
KeystrokeCount int
SessionCount int
PostSlug string
}
// RenderHumanProofBanner renders the public-facing banner HTML.
func RenderHumanProofBanner(hp *HumanProofBannerData) string {
return fmt.Sprintf(`<div id="hp-banner-%[4]s" data-human-proof-banner class="rounded-lg overflow-hidden my-6">`+
`<div class="flex items-center justify-between py-3 px-5 bg-muted border border-border rounded-lg">`+
`<div class="flex items-center gap-3">`+
`<div class="w-8 h-8 bg-primary text-primary-foreground rounded-md flex items-center justify-center text-xs font-bold">HP</div>`+
`<div>`+
`<div class="font-semibold text-sm text-foreground">Human Proof</div>`+
`<div class="text-xs text-muted-foreground">%[1]d min active &middot; %[2]s keystrokes &middot; %[3]d sessions</div>`+
`</div></div>`+
`<button class="px-4 py-1.5 bg-primary text-primary-foreground rounded-md text-sm font-medium cursor-pointer hover:bg-primary/90 border-0" data-action="watch">Watch this post being written</button>`+
`</div></div>`+
`<script src="/assets/human-proof-player.js" defer></script>`+
`<script>document.addEventListener("DOMContentLoaded",function(){if(window.HumanProof){window.HumanProof.init({postSlug:%[4]q,bannerId:"hp-banner-%[4]s",contentId:"hp-content-%[4]s"})}});</script>`,
hp.ActiveTimeMinutes,
formatThousands(hp.KeystrokeCount),
hp.SessionCount,
hp.PostSlug,
)
}
func formatThousands(n int) string {
if n < 1000 {
return fmt.Sprintf("%d", n)
}
s := fmt.Sprintf("%d", n)
var result []byte
for i, c := range s {
if i > 0 && (len(s)-i)%3 == 0 {
result = append(result, ',')
}
result = append(result, byte(c))
}
return string(result)
}
// ResolveMediaPath converts a media: prefixed path to a /media/ URL path.
func ResolveMediaPath(path string) string {
if after, ok := strings.CutPrefix(path, "media:"); ok {
return "/media/" + after
}
if _, err := uuid.Parse(path); err == nil {
return "/media/" + path
}
return path
}