diff --git a/elem.go b/elem.go index 56be5ff..a393439 100644 --- a/elem.go +++ b/elem.go @@ -92,15 +92,15 @@ func (n NoneNode) RenderWithOptions(opts RenderOptions) string { type TextNode string func (t TextNode) RenderTo(builder *strings.Builder, opts RenderOptions) { - builder.WriteString(string(t)) + builder.WriteString(EscapeNodeContents(string(t))) } func (t TextNode) Render() string { - return string(t) + return EscapeNodeContents(string(t)) } func (t TextNode) RenderWithOptions(opts RenderOptions) string { - return string(t) + return EscapeNodeContents(string(t)) } type RawNode string diff --git a/elements.go b/elements.go index 6b57945..066215d 100644 --- a/elements.go +++ b/elements.go @@ -143,7 +143,8 @@ func U(attrs attrs.Props, children ...Node) *Element { return newElement("u", attrs, children...) } -// Text creates a TextNode. +// Text creates a TextNode, content is automatically escaped to ensure safe rendering. +// For unescaped HTML, use Raw function instead. func Text(content string) TextNode { return TextNode(content) } diff --git a/elements_test.go b/elements_test.go index 223163c..369103c 100644 --- a/elements_test.go +++ b/elements_test.go @@ -673,6 +673,24 @@ func TestNoneInDiv(t *testing.T) { assert.Equal(t, expected, actual) } +func TestText(t *testing.T) { + expected := `

Hello, World!

` + el := P(nil, Text("Hello, World!")) + assert.Equal(t, expected, el.Render()) +} + +func TestTextWithEscaping(t *testing.T) { + expected := `

Hello, <em>World!</em>

` + el := P(nil, Text("Hello, World!")) + assert.Equal(t, expected, el.Render()) +} + +func TestTextWithNoQuotesEscaping(t *testing.T) { + expected := `

'Hello,' "World!"

` + el := P(nil, Text(`'Hello,' "World!"`)) + assert.Equal(t, expected, el.Render()) +} + func TestRaw(t *testing.T) { rawHTML := `

Test paragraph

` el := Raw(rawHTML) diff --git a/utils.go b/utils.go index 02d99a9..782da40 100644 --- a/utils.go +++ b/utils.go @@ -1,5 +1,16 @@ package elem +import "strings" + +// nodeContentReplacer handles escaping of HTML special characters in text nodes +// note that single and double quotes (', ") do not need escaping in HTML5 text nodes +var nodeContentReplacer = strings.NewReplacer( + "&", "&", + "<", "<", + // technically, > does not need escaping in HTML5, but we do it for consistency + ">", ">", +) + // If conditionally renders one of the provided elements based on the condition func If[T any](condition bool, ifTrue, ifFalse T) T { if condition { @@ -16,3 +27,8 @@ func TransformEach[T any](items []T, fn func(T) Node) []Node { } return nodes } + +// EscapeNodeContents escapes HTML5 special characters in a string to ensure safe rendering as a text node +func EscapeNodeContents(s string) string { + return nodeContentReplacer.Replace(s) +}