diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index 431f82f63..9e88ee950 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -55,18 +55,20 @@ func funcMap() template.FuncMap { // Add some extra functionality extra := template.FuncMap{ - "toToml": toTOML, - "mustToToml": mustToTOML, - "fromToml": fromTOML, - "toYaml": toYAML, - "mustToYaml": mustToYAML, - "toYamlPretty": toYAMLPretty, - "fromYaml": fromYAML, - "fromYamlArray": fromYAMLArray, - "toJson": toJSON, - "mustToJson": mustToJSON, - "fromJson": fromJSON, - "fromJsonArray": fromJSONArray, + "toToml": toTOML, + "mustToToml": mustToTOML, + "fromToml": fromTOML, + "toYaml": toYAML, + "mustToYaml": mustToYAML, + "toYamlPretty": toYAMLPretty, + "fromYaml": fromYAML, + "fromYamlArray": fromYAMLArray, + "toJson": toJSON, + "mustToJson": mustToJSON, + "toPrettyRawJson": toPrettyRawJSON, + "mustToPrettyRawJson": mustToPrettyRawJSON, + "fromJson": fromJSON, + "fromJsonArray": fromJSONArray, // Duration helpers "mustToDuration": mustToDuration, @@ -239,6 +241,56 @@ func mustToJSON(v any) string { return string(data) } +// encodePrettyRawJSON encodes v as indented JSON without HTML-escaping special +// characters (&, <, >). It uses two-space indentation to match the indentation +// of Sprig's toPrettyJson, while leaving HTML characters unescaped like Sprig's +// toRawJson. +func encodePrettyRawJSON(v any) (string, error) { + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + if err := enc.Encode(v); err != nil { + return "", err + } + return strings.TrimSuffix(buf.String(), "\n"), nil +} + +// toPrettyRawJSON takes an interface, marshals it to indented JSON without +// HTML-escaping special characters (&, <, >), and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// Unlike Sprig's toPrettyJson, HTML characters are not escaped. This is the +// indented counterpart to Sprig's toRawJson. The escaping behavior of +// toPrettyJson is intentionally left unchanged for backwards compatibility. +// +// This is designed to be called from a template. +func toPrettyRawJSON(v any) string { + s, err := encodePrettyRawJSON(v) + if err != nil { + // Swallow errors inside of a template. + return "" + } + return s +} + +// mustToPrettyRawJSON takes an interface, marshals it to indented JSON without +// HTML-escaping special characters (&, <, >), and returns a string. +// It will panic if there is an error. +// +// Unlike Sprig's mustToPrettyJson, HTML characters are not escaped. This is the +// indented counterpart to Sprig's mustToRawJson. +// +// This is designed to be called from a template when you need to ensure that +// the output JSON is valid. +func mustToPrettyRawJSON(v any) string { + s, err := encodePrettyRawJSON(v) + if err != nil { + panic(err) + } + return s +} + // fromJSON converts a JSON document into a map[string]interface{}. // // This is not a general-purpose JSON parser, and will not parse all valid diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index cf6a8d5c9..66effe841 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -72,6 +72,21 @@ keyInElement1 = "valueInElement1"`, tpl: `{{ toJson . }}`, expect: `{"foo":"bar"}`, vars: map[string]any{"foo": "bar"}, + }, { + // toPrettyRawJson must not HTML-escape &, <, > and must use 2-space indent + tpl: "{{ toPrettyRawJson . }}", + expect: "{\n \"url\": \"https://example.com?a=1&b=2<>\"\n}", + vars: map[string]any{"url": "https://example.com?a=1&b=2<>"}, + }, { + tpl: "{{ toPrettyRawJson . }}", + expect: "{\n \"foo\": \"bar\"\n}", + vars: map[string]any{"foo": "bar"}, + }, { + // toPrettyJson (from Sprig) must keep HTML-escaping &, <, > for + // backwards compatibility. + tpl: "{{ toPrettyJson . }}", + expect: "{\n \"url\": \"https://example.com?a=1\\u0026b=2\\u003c\\u003e\"\n}", + vars: map[string]any{"url": "https://example.com?a=1&b=2<>"}, }, { tpl: `{{ fromYaml . }}`, expect: "map[hello:world]", @@ -154,6 +169,17 @@ keyInElement1 = "valueInElement1"`, }, { tpl: `{{ mustToJson . }}`, vars: loopMap, + }, { + tpl: `{{ mustToPrettyRawJson . }}`, + vars: loopMap, // circular reference must panic + }, { + tpl: `{{ mustToPrettyRawJson . }}`, + expect: "{\n \"foo\": \"bar\"\n}", + vars: map[string]any{"foo": "bar"}, + }, { + tpl: `{{ toPrettyRawJson . }}`, + expect: "", // circular reference must swallow error and return "" + vars: loopMap, }, { tpl: `{{ mustToDuration 30 }}`, expect: `30s`,