diff --git a/context.go b/context.go index a74aeb5..19cddfb 100644 --- a/context.go +++ b/context.go @@ -1,10 +1,14 @@ package html +import i18n "forge.lthn.ai/core/go-i18n" + // Context carries rendering state through the node tree. type Context struct { - Identity string - Locale string - Data map[string]any + Identity string + Locale string + Entitlements func(feature string) bool + Data map[string]any + service *i18n.Service } // NewContext creates a new rendering context with sensible defaults. @@ -13,3 +17,11 @@ func NewContext() *Context { Data: make(map[string]any), } } + +// NewContextWithService creates a rendering context backed by a specific i18n service. +func NewContextWithService(svc *i18n.Service) *Context { + return &Context{ + Data: make(map[string]any), + service: svc, + } +} diff --git a/go.mod b/go.mod index e20be72..01fbda6 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,7 @@ module forge.lthn.ai/core/go-html go 1.25.5 replace forge.lthn.ai/core/go-i18n => ../go-i18n + +require forge.lthn.ai/core/go-i18n v0.0.0-00010101000000-000000000000 + +require golang.org/x/text v0.33.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..62a1313 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= diff --git a/node.go b/node.go index f4599d3..611a2d9 100644 --- a/node.go +++ b/node.go @@ -1,6 +1,10 @@ package html -import "strings" +import ( + "strings" + + i18n "forge.lthn.ai/core/go-i18n" +) // Node is anything renderable. type Node interface { @@ -96,3 +100,38 @@ func (n *elNode) Render(ctx *Context) string { return b.String() } + +// --- escapeHTML --- + +// escapeHTML escapes a string for safe inclusion in HTML text content. +func escapeHTML(s string) string { + s = strings.ReplaceAll(s, "&", "&") + s = strings.ReplaceAll(s, "<", "<") + s = strings.ReplaceAll(s, ">", ">") + s = strings.ReplaceAll(s, "\"", """) + s = strings.ReplaceAll(s, "'", "'") + return s +} + +// --- textNode --- + +type textNode struct { + key string + args []any +} + +// Text creates a node that renders through the go-i18n grammar pipeline. +// Output is HTML-escaped by default. Safe-by-default path. +func Text(key string, args ...any) Node { + return &textNode{key: key, args: args} +} + +func (n *textNode) Render(ctx *Context) string { + var text string + if ctx != nil && ctx.service != nil { + text = ctx.service.T(n.key, n.args...) + } else { + text = i18n.T(n.key, n.args...) + } + return escapeHTML(text) +} diff --git a/node_test.go b/node_test.go index aab8d72..8e54e23 100644 --- a/node_test.go +++ b/node_test.go @@ -1,6 +1,9 @@ package html -import "testing" +import ( + "strings" + "testing" +) func TestRawNode_Render(t *testing.T) { ctx := NewContext() @@ -50,3 +53,24 @@ func TestElNode_VoidElement(t *testing.T) { t.Errorf("El(\"br\").Render() = %q, want %q", got, want) } } + +func TestTextNode_Render(t *testing.T) { + ctx := NewContext() + node := Text("hello") + got := node.Render(ctx) + if got != "hello" { + t.Errorf("Text(\"hello\").Render() = %q, want %q", got, "hello") + } +} + +func TestTextNode_Escapes(t *testing.T) { + ctx := NewContext() + node := Text("") + got := node.Render(ctx) + if strings.Contains(got, "