Repository: gohugoio/hugo (language: go)

Issue:
transform.Plainify should return template.HTML instead of a string
<!-- Please answer these questions before submitting your issue. Thanks! -->

### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.84.0+extended linux/amd64 BuildDate=unknown
</pre>

### Does this issue reproduce with the latest release?

This is the latest release.

When using `plainify` hugo many times does the wrong thing. I realize that this is a feature, but I wonder if there is a better way to do it. Specifically, if the HTML contains any HTML entities `plainify` returns the plain HTML with the entities but not marked as a HTML safe string. So the `plainify` output for HTML like `<div>This &amp; that</div>` will be `This &amp; that`. Now it's easy to pipe it through `safeHTML` or `markdownify` or `htmlUnescape` to solve the problem, but it seems `plainify` should probably include an automatic `htmlUnescape` or at least mark the output as HTML safe.
This is a petty kind of thing, but in our code, we always must do `plainify | htmlUnescape` and whenever we forget it the entities are shown on the screen. Seems cumbersome.


Discussion / hints:
You should open that in the discourse instead of here. 

Regarding the issue at hand: I think it's because of the security model of Golang templates. Go expects the developer to make everything safe, so plainify can't just do an html unescape or mark the string as safe. I think it was recently only that script and style tags were removed by plainify. Lot's of hijinks would have been possible if Go just assumes that the string is safe. 

See here: https://golang.org/pkg/html/template/#hdr-Security_Model (the whole chapter is an interesting read)
Yes I don't like the safeHTML either, maybe to include htmlUnescape would be safer and useful. What is the current use case for plainify? In my code 100% of my plainify calls have to be followed by one of the other filters. Maybe there should be a new method to extract plain text from html and return it as unescaped string rather than messing with plainfy.
These transformation functions return `template.HTML`

- transform.Emojify
- transform.Highlight
- transform.Markdownify

I think it makes sense to add `transform.Plainify` to the list.

Fix patch:
diff --git a/docs/content/en/functions/transform/Plainify.md b/docs/content/en/functions/transform/Plainify.md
index 040145170b8..681d41f72e2 100644
--- a/docs/content/en/functions/transform/Plainify.md
+++ b/docs/content/en/functions/transform/Plainify.md
@@ -6,7 +6,7 @@ keywords: []
 action:
   aliases: [plainify]
   related: []
-  returnType: string
+  returnType: template.HTML
   signatures: [transform.Plainify INPUT]
 aliases: [/functions/plainify]
 ---
diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go
index db7703b7fb5..10a91671d54 100644
--- a/tpl/transform/transform.go
+++ b/tpl/transform/transform.go
@@ -54,7 +54,7 @@ func New(deps *deps.Deps) *Namespace {
 			"/tmpl/transform/unmarshal",
 			dynacache.OptionsPartition{Weight: 30, ClearWhen: dynacache.ClearOnChange},
 		),
-		cacheMath: dynacache.GetOrCreatePartition[string, string](
+		cacheMath: dynacache.GetOrCreatePartition[string, template.HTML](
 			deps.MemCache,
 			"/tmpl/transform/math",
 			dynacache.OptionsPartition{Weight: 30, ClearWhen: dynacache.ClearNever},
@@ -65,7 +65,7 @@ func New(deps *deps.Deps) *Namespace {
 // Namespace provides template functions for the "transform" namespace.
 type Namespace struct {
 	cacheUnmarshal *dynacache.Partition[string, *resources.StaleValue[any]]
-	cacheMath      *dynacache.Partition[string, string]
+	cacheMath      *dynacache.Partition[string, template.HTML]
 
 	id   atomic.Uint32
 	deps *deps.Deps
@@ -188,18 +188,18 @@ func (ns *Namespace) Markdownify(ctx context.Context, s any) (template.HTML, err
 }
 
 // Plainify returns a copy of s with all HTML tags removed.
-func (ns *Namespace) Plainify(s any) (string, error) {
+func (ns *Namespace) Plainify(s any) (template.HTML, error) {
 	ss, err := cast.ToStringE(s)
 	if err != nil {
 		return "", err
 	}
 
-	return tpl.StripHTML(ss), nil
+	return template.HTML(tpl.StripHTML(ss)), nil
 }
 
 // ToMath converts a LaTeX string to math in the given format, default MathML.
 // This uses KaTeX to render the math, see https://katex.org/.
-func (ns *Namespace) ToMath(ctx context.Context, args ...any) (string, error) {
+func (ns *Namespace) ToMath(ctx context.Context, args ...any) (template.HTML, error) {
 	if len(args) < 1 {
 		return "", errors.New("must provide at least one argument")
 	}
@@ -226,7 +226,7 @@ func (ns *Namespace) ToMath(ctx context.Context, args ...any) (string, error) {
 	key := "tomath/" + s[:2] + "/" + s[2:]
 	fileCache := ns.deps.ResourceSpec.FileCaches.MiscCache()
 
-	return ns.cacheMath.GetOrCreate(key, func(string) (string, error) {
+	return ns.cacheMath.GetOrCreate(key, func(string) (template.HTML, error) {
 		_, r, err := fileCache.GetOrCreate(key, func() (io.ReadCloser, error) {
 			message := warpc.Message[warpc.KatexInput]{
 				Header: warpc.Header{
@@ -250,7 +250,9 @@ func (ns *Namespace) ToMath(ctx context.Context, args ...any) (string, error) {
 			return "", err
 		}
 
-		return hugio.ReadString(r)
+		s, err := hugio.ReadString(r)
+
+		return template.HTML(s), err
 	})
 }
 


Test patch:
diff --git a/tpl/transform/transform_integration_test.go b/tpl/transform/transform_integration_test.go
index 529f18a5f17..b030417a234 100644
--- a/tpl/transform/transform_integration_test.go
+++ b/tpl/transform/transform_integration_test.go
@@ -141,8 +141,7 @@ func TestToMath(t *testing.T) {
 -- hugo.toml --
 disableKinds = ['page','rss','section','sitemap','taxonomy','term']
 -- layouts/index.html --
-{{ $result := transform.ToMath "c = \\pm\\sqrt{a^2 + b^2}" }}
-{{ printf "%v" $result | safeHTML }}
+{{ transform.ToMath "c = \\pm\\sqrt{a^2 + b^2}" }}
   `
 	b := hugolib.Test(t, files)
 
diff --git a/tpl/transform/transform_test.go b/tpl/transform/transform_test.go
index d645ca8e2eb..1f6c99ec4f7 100644
--- a/tpl/transform/transform_test.go
+++ b/tpl/transform/transform_test.go
@@ -244,6 +244,6 @@ func TestPlainify(t *testing.T) {
 		}
 
 		b.Assert(err, qt.IsNil)
-		b.Assert(result, qt.Equals, test.expect)
+		b.Assert(result, qt.Equals, template.HTML(test.expect.(string)))
 	}
 }