From 4698071a9dedc3154056365693c06f73a9162b7f Mon Sep 17 00:00:00 2001 From: Alex Svetkin Date: Sat, 12 Jul 2025 14:16:03 +0200 Subject: [PATCH 1/2] added content escaping for text nodes --- elem.go | 6 +++--- elements.go | 3 ++- elements_test.go | 18 ++++++++++++++++++ utils.go | 13 +++++++++++++ 4 files changed, 36 insertions(+), 4 deletions(-) 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..1321995 100644 --- a/utils.go +++ b/utils.go @@ -1,5 +1,7 @@ package elem +import "strings" + // 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 +18,14 @@ 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 { + s = strings.ReplaceAll(s, "&", "&") + s = strings.ReplaceAll(s, "<", "<") + // technically, > does not need escaping in HTML5, but we do it for consistency + s = strings.ReplaceAll(s, ">", ">") + // note that single and double quotes (', ") do not need escaping in HTML5 text nodes + + return s +} From f35d77934de811003dc7314ead3d6fb18d777894 Mon Sep 17 00:00:00 2001 From: Alex Svetkin Date: Tue, 15 Jul 2025 10:53:40 +0200 Subject: [PATCH 2/2] string replace optimizations --- utils.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/utils.go b/utils.go index 1321995..782da40 100644 --- a/utils.go +++ b/utils.go @@ -2,6 +2,15 @@ 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 { @@ -21,11 +30,5 @@ func TransformEach[T any](items []T, fn func(T) Node) []Node { // EscapeNodeContents escapes HTML5 special characters in a string to ensure safe rendering as a text node func EscapeNodeContents(s string) string { - s = strings.ReplaceAll(s, "&", "&") - s = strings.ReplaceAll(s, "<", "<") - // technically, > does not need escaping in HTML5, but we do it for consistency - s = strings.ReplaceAll(s, ">", ">") - // note that single and double quotes (', ") do not need escaping in HTML5 text nodes - - return s + return nodeContentReplacer.Replace(s) }