feat: add conditional, entitlement, switch, and each nodes
Implements If, Unless, Entitled, Switch, and Each (generic) control flow nodes. Entitled uses deny-by-default when no entitlement function is set, rendering absent content rather than hidden content. Each uses Go generics for type-safe iteration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3e76e72cb7
commit
c7240943d7
2 changed files with 195 additions and 0 deletions
101
node.go
101
node.go
|
|
@ -135,3 +135,104 @@ func (n *textNode) Render(ctx *Context) string {
|
|||
}
|
||||
return escapeHTML(text)
|
||||
}
|
||||
|
||||
// --- ifNode ---
|
||||
|
||||
type ifNode struct {
|
||||
cond func(*Context) bool
|
||||
node Node
|
||||
}
|
||||
|
||||
// If renders child only when condition is true.
|
||||
func If(cond func(*Context) bool, node Node) Node {
|
||||
return &ifNode{cond: cond, node: node}
|
||||
}
|
||||
|
||||
func (n *ifNode) Render(ctx *Context) string {
|
||||
if n.cond(ctx) {
|
||||
return n.node.Render(ctx)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// --- unlessNode ---
|
||||
|
||||
type unlessNode struct {
|
||||
cond func(*Context) bool
|
||||
node Node
|
||||
}
|
||||
|
||||
// Unless renders child only when condition is false.
|
||||
func Unless(cond func(*Context) bool, node Node) Node {
|
||||
return &unlessNode{cond: cond, node: node}
|
||||
}
|
||||
|
||||
func (n *unlessNode) Render(ctx *Context) string {
|
||||
if !n.cond(ctx) {
|
||||
return n.node.Render(ctx)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// --- entitledNode ---
|
||||
|
||||
type entitledNode struct {
|
||||
feature string
|
||||
node Node
|
||||
}
|
||||
|
||||
// Entitled renders child only when entitlement is granted. Absent, not hidden.
|
||||
// If no entitlement function is set on the context, access is denied by default.
|
||||
func Entitled(feature string, node Node) Node {
|
||||
return &entitledNode{feature: feature, node: node}
|
||||
}
|
||||
|
||||
func (n *entitledNode) Render(ctx *Context) string {
|
||||
if ctx == nil || ctx.Entitlements == nil || !ctx.Entitlements(n.feature) {
|
||||
return ""
|
||||
}
|
||||
return n.node.Render(ctx)
|
||||
}
|
||||
|
||||
// --- switchNode ---
|
||||
|
||||
type switchNode struct {
|
||||
selector func(*Context) string
|
||||
cases map[string]Node
|
||||
}
|
||||
|
||||
// Switch renders based on runtime selector value.
|
||||
func Switch(selector func(*Context) string, cases map[string]Node) Node {
|
||||
return &switchNode{selector: selector, cases: cases}
|
||||
}
|
||||
|
||||
func (n *switchNode) Render(ctx *Context) string {
|
||||
key := n.selector(ctx)
|
||||
if node, ok := n.cases[key]; ok {
|
||||
return node.Render(ctx)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// --- eachNode ---
|
||||
|
||||
type eachNode[T any] struct {
|
||||
items []T
|
||||
fn func(T) Node
|
||||
}
|
||||
|
||||
// Each iterates items and renders each via fn.
|
||||
func Each[T any](items []T, fn func(T) Node) Node {
|
||||
return &eachNode[T]{items: items, fn: fn}
|
||||
}
|
||||
|
||||
func (n *eachNode[T]) Render(ctx *Context) string {
|
||||
if len(n.items) == 0 {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, item := range n.items {
|
||||
b.WriteString(n.fn(item).Render(ctx))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
|
|
|||
94
node_test.go
94
node_test.go
|
|
@ -74,3 +74,97 @@ func TestTextNode_Escapes(t *testing.T) {
|
|||
t.Errorf("Text node should contain escaped script tag, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIfNode_True(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
node := If(func(*Context) bool { return true }, Raw("visible"))
|
||||
got := node.Render(ctx)
|
||||
if got != "visible" {
|
||||
t.Errorf("If(true) = %q, want %q", got, "visible")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIfNode_False(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
node := If(func(*Context) bool { return false }, Raw("hidden"))
|
||||
got := node.Render(ctx)
|
||||
if got != "" {
|
||||
t.Errorf("If(false) = %q, want %q", got, "")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnlessNode(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
node := Unless(func(*Context) bool { return false }, Raw("visible"))
|
||||
got := node.Render(ctx)
|
||||
if got != "visible" {
|
||||
t.Errorf("Unless(false) = %q, want %q", got, "visible")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntitledNode_Granted(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
ctx.Entitlements = func(feature string) bool { return feature == "premium" }
|
||||
node := Entitled("premium", Raw("premium content"))
|
||||
got := node.Render(ctx)
|
||||
if got != "premium content" {
|
||||
t.Errorf("Entitled(granted) = %q, want %q", got, "premium content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntitledNode_Denied(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
ctx.Entitlements = func(feature string) bool { return false }
|
||||
node := Entitled("premium", Raw("premium content"))
|
||||
got := node.Render(ctx)
|
||||
if got != "" {
|
||||
t.Errorf("Entitled(denied) = %q, want %q", got, "")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntitledNode_NoFunc(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
node := Entitled("premium", Raw("premium content"))
|
||||
got := node.Render(ctx)
|
||||
if got != "" {
|
||||
t.Errorf("Entitled(no func) = %q, want %q (deny by default)", got, "")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEachNode(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
items := []string{"a", "b", "c"}
|
||||
node := Each(items, func(item string) Node {
|
||||
return El("li", Raw(item))
|
||||
})
|
||||
got := node.Render(ctx)
|
||||
want := "<li>a</li><li>b</li><li>c</li>"
|
||||
if got != want {
|
||||
t.Errorf("Each([a,b,c]) = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEachNode_Empty(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
node := Each([]string{}, func(item string) Node {
|
||||
return El("li", Raw(item))
|
||||
})
|
||||
got := node.Render(ctx)
|
||||
if got != "" {
|
||||
t.Errorf("Each([]) = %q, want %q", got, "")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwitchNode(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
cases := map[string]Node{
|
||||
"dark": Raw("dark theme"),
|
||||
"light": Raw("light theme"),
|
||||
}
|
||||
node := Switch(func(*Context) string { return "dark" }, cases)
|
||||
got := node.Render(ctx)
|
||||
want := "dark theme"
|
||||
if got != want {
|
||||
t.Errorf("Switch(\"dark\") = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue