diff --git a/docs/content/en/functions/jsonify.md b/docs/content/en/functions/jsonify.md index 28b90534c..3aa38c8c4 100644 --- a/docs/content/en/functions/jsonify.md +++ b/docs/content/en/functions/jsonify.md @@ -32,6 +32,17 @@ more copies of *indent* according to the indentation nesting. {{ dict "title" .Title "content" .Plain | jsonify (dict "prefix" " " "indent" " ") }} ``` +## Jsonify options + +indent ("") +: Indendation to use. + +prefix ("") +: Indentation prefix. + +noHTMLEscape (false) +: Disable escaping of problematic HTML characters inside JSON quoted strings. The default behavior is to escape &, <, and > to \u0026, \u003c, and \u003e to avoid certain safety problems that can arise when embedding JSON in HTML. + See also the `.PlainWords`, `.Plain`, and `.RawContent` [page variables][pagevars]. [pagevars]: /variables/page/ diff --git a/tpl/encoding/encoding.go b/tpl/encoding/encoding.go index 272503e0c..0510f86e2 100644 --- a/tpl/encoding/encoding.go +++ b/tpl/encoding/encoding.go @@ -20,7 +20,10 @@ import ( "errors" "html/template" + bp "github.com/gohugoio/hugo/bufferpool" + "github.com/gohugoio/hugo/common/maps" + "github.com/mitchellh/mapstructure" "github.com/spf13/cast" ) @@ -60,24 +63,27 @@ func (ns *Namespace) Base64Encode(content any) (string, error) { // to the indentation nesting. func (ns *Namespace) Jsonify(args ...any) (template.HTML, error) { var ( - b []byte - err error + b []byte + err error + obj any + opts jsonifyOpts ) switch len(args) { case 0: return "", nil case 1: - b, err = json.Marshal(args[0]) + obj = args[0] case 2: - var opts map[string]string - - opts, err = maps.ToStringMapStringE(args[0]) + var m map[string]any + m, err = maps.ToStringMapE(args[0]) if err != nil { break } - - b, err = json.MarshalIndent(args[1], opts["prefix"], opts["indent"]) + if err = mapstructure.WeakDecode(m, &opts); err != nil { + break + } + obj = args[1] default: err = errors.New("too many arguments to jsonify") } @@ -86,5 +92,25 @@ func (ns *Namespace) Jsonify(args ...any) (template.HTML, error) { return "", err } + buff := bp.GetBuffer() + defer bp.PutBuffer(buff) + e := json.NewEncoder(buff) + e.SetEscapeHTML(!opts.NoHTMLEscape) + e.SetIndent(opts.Prefix, opts.Indent) + if err = e.Encode(obj); err != nil { + return "", err + } + b = buff.Bytes() + // See https://github.com/golang/go/issues/37083 + // Hugo changed from MarshalIndent/Marshal. To make the output + // the same, we need to trim the trailing newline. + b = b[:len(b)-1] + return template.HTML(b), nil } + +type jsonifyOpts struct { + Prefix string + Indent string + NoHTMLEscape bool +} diff --git a/tpl/encoding/encoding_test.go b/tpl/encoding/encoding_test.go index e7c82e3be..8e6e2da48 100644 --- a/tpl/encoding/encoding_test.go +++ b/tpl/encoding/encoding_test.go @@ -82,7 +82,7 @@ func TestJsonify(t *testing.T) { c := qt.New(t) ns := New() - for _, test := range []struct { + for i, test := range []struct { opts any v any expect any @@ -91,6 +91,9 @@ func TestJsonify(t *testing.T) { {map[string]string{"indent": ""}, []string{"a", "b"}, template.HTML("[\n\"a\",\n\"b\"\n]")}, {map[string]string{"prefix": "

"}, []string{"a", "b"}, template.HTML("[\n

\"a\",\n

\"b\"\n

]")}, {map[string]string{"prefix": "

", "indent": ""}, []string{"a", "b"}, template.HTML("[\n

\"a\",\n

\"b\"\n

]")}, + {map[string]string{"indent": ""}, []string{"a", "b"}, template.HTML("[\n\"a\",\n\"b\"\n]")}, + {map[string]any{"noHTMLEscape": false}, []string{"", ""}, template.HTML("[\"\\u003ca\\u003e\",\"\\u003cb\\u003e\"]")}, + {map[string]any{"noHTMLEscape": true}, []string{"", ""}, template.HTML("[\"\",\"\"]")}, {nil, tstNoStringer{}, template.HTML("{}")}, {nil, nil, template.HTML("null")}, // errors @@ -108,11 +111,11 @@ func TestJsonify(t *testing.T) { result, err := ns.Jsonify(args...) if b, ok := test.expect.(bool); ok && !b { - c.Assert(err, qt.Not(qt.IsNil)) + c.Assert(err, qt.Not(qt.IsNil), qt.Commentf("#%d", i)) continue } c.Assert(err, qt.IsNil) - c.Assert(result, qt.Equals, test.expect) + c.Assert(result, qt.Equals, test.expect, qt.Commentf("#%d", i)) } }