feat: add Text node with go-i18n grammar pipeline
Introduces textNode that renders through go-i18n's T() function with automatic HTML escaping for safe-by-default output. Adds escapeHTML utility, NewContextWithService for explicit service binding, and Entitlements field on Context for upcoming conditional rendering. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d7bb0b2b2c
commit
3e76e72cb7
5 changed files with 86 additions and 5 deletions
18
context.go
18
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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
go.mod
4
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
|
||||
|
|
|
|||
2
go.sum
Normal file
2
go.sum
Normal file
|
|
@ -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=
|
||||
41
node.go
41
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)
|
||||
}
|
||||
|
|
|
|||
26
node_test.go
26
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("<script>alert('xss')</script>")
|
||||
got := node.Render(ctx)
|
||||
if strings.Contains(got, "<script>") {
|
||||
t.Errorf("Text node must HTML-escape output, got %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "<script>") {
|
||||
t.Errorf("Text node should contain escaped script tag, got %q", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue