diff --git a/node.go b/node.go index 611a2d9..725cc86 100644 --- a/node.go +++ b/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() +} diff --git a/node_test.go b/node_test.go index 8e54e23..cfca0c1 100644 --- a/node_test.go +++ b/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 := "
  • a
  • b
  • c
  • " + 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) + } +}