summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/content/en/functions/transform/XMLEscape.md38
-rw-r--r--tpl/tplimpl/embedded/templates/_default/rss.xml2
-rw-r--r--tpl/tplimpl/template_funcs_test.go6
-rw-r--r--tpl/transform/init.go10
-rw-r--r--tpl/transform/integration_test.go20
-rw-r--r--tpl/transform/transform.go31
6 files changed, 105 insertions, 2 deletions
diff --git a/docs/content/en/functions/transform/XMLEscape.md b/docs/content/en/functions/transform/XMLEscape.md
new file mode 100644
index 000000000..17ed2a13d
--- /dev/null
+++ b/docs/content/en/functions/transform/XMLEscape.md
@@ -0,0 +1,38 @@
+---
+title: transform.XMLEscape
+description: Returns the given string, removing disallowed characters then escaping the result to its XML equivalent.
+categories: []
+keywords: []
+action:
+ aliases: []
+ related: []
+ returnType: string
+ signatures: [transform.XMLEscape INPUT]
+---
+
+The `transform.XMLEscape` function removes [disallowed characters] as defined in the XML specification, then escapes the result by replacing the following characters with [HTML entities]:
+
+- `"` → `"`
+- `'` → `'`
+- `&` → `&`
+- `<` → `&lt;`
+- `>` → `&gt;`
+- `\t` → `&#x9;`
+- `\n` → `&#xA;`
+- `\r` → `&#xD;`
+
+For example:
+
+```go-html-template
+transform.XMLEscape "<p>abc</p>" → &lt;p&gt;abc&lt;/p&gt;
+```
+
+When using `transform.XMLEscape` in a template rendered by Go's [html/template] package, declare the string to be safe HTML to avoid double escaping. For example, in an RSS template:
+
+{{< code file="layouts/_default/rss.xml" >}}
+<description>{{ .Summary | transform.XMLEscape | safeHTML }}</description>
+{{< /code >}}
+
+[disallowed characters]: https://www.w3.org/TR/xml/#charsets
+[html entities]: https://developer.mozilla.org/en-us/docs/glossary/entity
+[html/template]: https://pkg.go.dev/html/template
diff --git a/tpl/tplimpl/embedded/templates/_default/rss.xml b/tpl/tplimpl/embedded/templates/_default/rss.xml
index a4cdd2383..6818d31ec 100644
--- a/tpl/tplimpl/embedded/templates/_default/rss.xml
+++ b/tpl/tplimpl/embedded/templates/_default/rss.xml
@@ -64,7 +64,7 @@
<pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
{{- with $authorEmail }}<author>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</author>{{ end }}
<guid>{{ .Permalink }}</guid>
- <description>{{ .Summary | html }}</description>
+ <description>{{ .Summary | transform.XMLEscape | safeHTML }}</description>
</item>
{{- end }}
</channel>
diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go
index cb1aa6feb..9cc84934b 100644
--- a/tpl/tplimpl/template_funcs_test.go
+++ b/tpl/tplimpl/template_funcs_test.go
@@ -37,7 +37,7 @@ home=["HTML"]
-- files/README.txt --
Hugo Rocks!
-- content/blog/hugo-rocks.md --
----
+---
title: "**BatMan**"
---
`
@@ -65,6 +65,10 @@ title: "**BatMan**"
// This will fail the build, so skip for now.
continue
}
+ if strings.Contains(example[0], "transform.XMLEscape") {
+ // This will fail the build, so skip for now.
+ continue
+ }
templates = append(templates, example[0])
expected = append(expected, example[1])
}
diff --git a/tpl/transform/init.go b/tpl/transform/init.go
index 00ae8f89d..e43960427 100644
--- a/tpl/transform/init.go
+++ b/tpl/transform/init.go
@@ -112,6 +112,16 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.XMLEscape,
+ nil,
+ [][2]string{
+ {
+ `{{ transform.XMLEscape "<p>abc</p>" }}`,
+ `&lt;p&gt;abc&lt;/p&gt;`,
+ },
+ },
+ )
+
return ns
}
diff --git a/tpl/transform/integration_test.go b/tpl/transform/integration_test.go
index 17348928d..9a68b7ff2 100644
--- a/tpl/transform/integration_test.go
+++ b/tpl/transform/integration_test.go
@@ -65,3 +65,23 @@ foo
b.AssertFileContent("public/p3/index.html", "_<h2 id=\"foo\">foo</h2>\n<p>bar</p>\n_")
b.AssertFileContent("public/p4/index.html", "_<p id=\"bar\">foo</p>\n_")
}
+
+func TestXMLEscape(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- config.toml --
+disableKinds = ['section','sitemap','taxonomy','term']
+-- content/p1.md --
+---
+title: p1
+---
+a **b** c
+<!--more-->
+ `
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.xml", `
+ <description>&lt;p&gt;a &lt;strong&gt;b&lt;/strong&gt; c&lt;/p&gt;</description>
+ `)
+}
diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go
index d943b0b57..178db84e4 100644
--- a/tpl/transform/transform.go
+++ b/tpl/transform/transform.go
@@ -15,9 +15,12 @@
package transform
import (
+ "bytes"
"context"
+ "encoding/xml"
"html"
"html/template"
+ "strings"
"github.com/gohugoio/hugo/cache/namedmemcache"
"github.com/gohugoio/hugo/markup/converter/hooks"
@@ -118,6 +121,34 @@ func (ns *Namespace) HTMLUnescape(s any) (string, error) {
return html.UnescapeString(ss), nil
}
+// XMLEscape returns the given string, removing disallowed characters then
+// escaping the result to its XML equivalent.
+func (ns *Namespace) XMLEscape(s any) (string, error) {
+ ss, err := cast.ToStringE(s)
+ if err != nil {
+ return "", err
+ }
+
+ // https://www.w3.org/TR/xml/#NT-Char
+ cleaned := strings.Map(func(r rune) rune {
+ if r == 0x9 || r == 0xA || r == 0xD ||
+ (r >= 0x20 && r <= 0xD7FF) ||
+ (r >= 0xE000 && r <= 0xFFFD) ||
+ (r >= 0x10000 && r <= 0x10FFFF) {
+ return r
+ }
+ return -1
+ }, ss)
+
+ var buf bytes.Buffer
+ err = xml.EscapeText(&buf, []byte(cleaned))
+ if err != nil {
+ return "", err
+ }
+
+ return buf.String(), nil
+}
+
// Markdownify renders s from Markdown to HTML.
func (ns *Namespace) Markdownify(ctx context.Context, s any) (template.HTML, error) {