feat: scaffold go-html module with Node interface
Introduces the go-html module with a Node rendering interface, Raw and El constructors, void element handling, and attribute escaping. Includes a minimal Context stub and tests for all node types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
d7bb0b2b2c
4 changed files with 170 additions and 0 deletions
15
context.go
Normal file
15
context.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package html
|
||||
|
||||
// Context carries rendering state through the node tree.
|
||||
type Context struct {
|
||||
Identity string
|
||||
Locale string
|
||||
Data map[string]any
|
||||
}
|
||||
|
||||
// NewContext creates a new rendering context with sensible defaults.
|
||||
func NewContext() *Context {
|
||||
return &Context{
|
||||
Data: make(map[string]any),
|
||||
}
|
||||
}
|
||||
5
go.mod
Normal file
5
go.mod
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module forge.lthn.ai/core/go-html
|
||||
|
||||
go 1.25.5
|
||||
|
||||
replace forge.lthn.ai/core/go-i18n => ../go-i18n
|
||||
98
node.go
Normal file
98
node.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package html
|
||||
|
||||
import "strings"
|
||||
|
||||
// Node is anything renderable.
|
||||
type Node interface {
|
||||
Render(ctx *Context) string
|
||||
}
|
||||
|
||||
// voidElements is the set of HTML elements that must not have a closing tag.
|
||||
var voidElements = map[string]bool{
|
||||
"area": true,
|
||||
"base": true,
|
||||
"br": true,
|
||||
"col": true,
|
||||
"embed": true,
|
||||
"hr": true,
|
||||
"img": true,
|
||||
"input": true,
|
||||
"link": true,
|
||||
"meta": true,
|
||||
"source": true,
|
||||
"track": true,
|
||||
"wbr": true,
|
||||
}
|
||||
|
||||
// escapeAttr escapes a string for use in an HTML attribute value.
|
||||
func escapeAttr(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
|
||||
}
|
||||
|
||||
// --- rawNode ---
|
||||
|
||||
type rawNode struct {
|
||||
content string
|
||||
}
|
||||
|
||||
// Raw creates a node that renders without escaping (escape hatch for trusted content).
|
||||
func Raw(content string) Node {
|
||||
return &rawNode{content: content}
|
||||
}
|
||||
|
||||
func (n *rawNode) Render(_ *Context) string {
|
||||
return n.content
|
||||
}
|
||||
|
||||
// --- elNode ---
|
||||
|
||||
type elNode struct {
|
||||
tag string
|
||||
children []Node
|
||||
attrs map[string]string
|
||||
}
|
||||
|
||||
// El creates an HTML element node with children.
|
||||
func El(tag string, children ...Node) Node {
|
||||
return &elNode{
|
||||
tag: tag,
|
||||
children: children,
|
||||
attrs: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *elNode) Render(ctx *Context) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteByte('<')
|
||||
b.WriteString(n.tag)
|
||||
|
||||
for key, val := range n.attrs {
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(key)
|
||||
b.WriteString(`="`)
|
||||
b.WriteString(escapeAttr(val))
|
||||
b.WriteByte('"')
|
||||
}
|
||||
|
||||
b.WriteByte('>')
|
||||
|
||||
if voidElements[n.tag] {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
for _, child := range n.children {
|
||||
b.WriteString(child.Render(ctx))
|
||||
}
|
||||
|
||||
b.WriteString("</")
|
||||
b.WriteString(n.tag)
|
||||
b.WriteByte('>')
|
||||
|
||||
return b.String()
|
||||
}
|
||||
52
node_test.go
Normal file
52
node_test.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package html
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRawNode_Render(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
node := Raw("hello")
|
||||
got := node.Render(ctx)
|
||||
if got != "hello" {
|
||||
t.Errorf("Raw(\"hello\").Render() = %q, want %q", got, "hello")
|
||||
}
|
||||
}
|
||||
|
||||
func TestElNode_Render(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
node := El("div", Raw("content"))
|
||||
got := node.Render(ctx)
|
||||
want := "<div>content</div>"
|
||||
if got != want {
|
||||
t.Errorf("El(\"div\", Raw(\"content\")).Render() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestElNode_Nested(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
node := El("div", El("span", Raw("inner")))
|
||||
got := node.Render(ctx)
|
||||
want := "<div><span>inner</span></div>"
|
||||
if got != want {
|
||||
t.Errorf("nested El().Render() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestElNode_MultipleChildren(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
node := El("div", Raw("a"), Raw("b"))
|
||||
got := node.Render(ctx)
|
||||
want := "<div>ab</div>"
|
||||
if got != want {
|
||||
t.Errorf("El with multiple children = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestElNode_VoidElement(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
node := El("br")
|
||||
got := node.Render(ctx)
|
||||
want := "<br>"
|
||||
if got != want {
|
||||
t.Errorf("El(\"br\").Render() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue