From 2d69d9dafea6271bc43e307d56f28db5854ebd5c Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Fri, 20 Sep 2024 11:16:04 +0200 Subject: [PATCH] feat: allow user to disable HTML escaping when marshalling to JSON. Fixes https://github.com/wk8/go-ordered-map/issues/37 --- json.go | 20 +++++++++++++++++--- json_test.go | 20 ++++++++++++++++++++ orderedmap.go | 27 +++++++++++++++++++++------ yaml.go | 2 +- 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/json.go b/json.go index 53f176a..cd09edc 100644 --- a/json.go +++ b/json.go @@ -23,7 +23,9 @@ func (om *OrderedMap[K, V]) MarshalJSON() ([]byte, error) { //nolint:funlen return []byte("null"), nil } - writer := jwriter.Writer{} + writer := jwriter.Writer{ + NoEscapeHTML: om.disableHTMLEscape, + } writer.RawByte('{') for pair, firstIteration := om.Oldest(), true; pair != nil; pair = pair.Next() { @@ -78,7 +80,7 @@ func (om *OrderedMap[K, V]) MarshalJSON() ([]byte, error) { //nolint:funlen writer.RawByte(':') // the error is checked at the end of the function - writer.Raw(json.Marshal(pair.Value)) + writer.Raw(jsonMarshal(pair.Value, om.disableHTMLEscape)) } writer.RawByte('}') @@ -86,6 +88,18 @@ func (om *OrderedMap[K, V]) MarshalJSON() ([]byte, error) { //nolint:funlen return dumpWriter(&writer) } +func jsonMarshal(t interface{}, disableHTMlEscape bool) ([]byte, error) { + if disableHTMlEscape { + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(false) + err := encoder.Encode(t) + // Encode() adds an extra newline, strip it off to guarantee same behavior as json.Marshal + return bytes.TrimRight(buffer.Bytes(), "\n"), err + } + return json.Marshal(t) +} + func dumpWriter(writer *jwriter.Writer) ([]byte, error) { if writer.Error != nil { return nil, writer.Error @@ -103,7 +117,7 @@ func dumpWriter(writer *jwriter.Writer) ([]byte, error) { // UnmarshalJSON implements the json.Unmarshaler interface. func (om *OrderedMap[K, V]) UnmarshalJSON(data []byte) error { if om.list == nil { - om.initialize(0) + om.initialize(0, om.disableHTMLEscape) } return jsonparser.ObjectEach( diff --git a/json_test.go b/json_test.go index 42b89ab..bfb08b7 100644 --- a/json_test.go +++ b/json_test.go @@ -107,6 +107,26 @@ func TestMarshalJSON(t *testing.T) { assert.NoError(t, err) assert.Equal(t, `{}`, string(b)) }) + + t.Run("HTML escaping enabled (default)", func(t *testing.T) { + om := New[marshallable, any]() + om.Set(marshallable(1), "hello this is bold") + om.Set(marshallable(28), "some book") + + b, err := jsonMarshal(om, false) + assert.NoError(t, err) + assert.Equal(t, `{"#1#":"hello \u003cstrong\u003ethis is bold\u003c/strong\u003e","#28#":"\u003c?xml version=\"1.0\"?\u003e\u003ccatalog\u003e\u003cbook\u003esome book\u003c/book\u003e\u003c/catalog\u003e"}`, string(b)) + }) + + t.Run("HTML escaping disabled", func(t *testing.T) { + om := New[marshallable, any](WithDisableHTMLEscape[marshallable, any]()) + om.Set(marshallable(1), "hello this is bold") + om.Set(marshallable(28), "some book") + + b, err := jsonMarshal(om, true /* we need to disable HTML escaping here also */) + assert.NoError(t, err) + assert.Equal(t, `{"#1#":"hello this is bold","#28#":"some book"}`, string(b)) + }) } func TestUnmarshallJSON(t *testing.T) { diff --git a/orderedmap.go b/orderedmap.go index 45bf862..6198eb6 100644 --- a/orderedmap.go +++ b/orderedmap.go @@ -21,13 +21,15 @@ type Pair[K comparable, V any] struct { } type OrderedMap[K comparable, V any] struct { - pairs map[K]*Pair[K, V] - list *list.List[*Pair[K, V]] + pairs map[K]*Pair[K, V] + list *list.List[*Pair[K, V]] + disableHTMLEscape bool } type initConfig[K comparable, V any] struct { - capacity int - initialData []Pair[K, V] + capacity int + initialData []Pair[K, V] + disableHTMLEscape bool } type InitOption[K comparable, V any] func(config *initConfig[K, V]) @@ -49,6 +51,13 @@ func WithInitialData[K comparable, V any](initialData ...Pair[K, V]) InitOption[ } } +// WithDisableHTMLEscape disables HTMl escaping when marshalling to JSON +func WithDisableHTMLEscape[K comparable, V any]() InitOption[K, V] { + return func(c *initConfig[K, V]) { + c.disableHTMLEscape = true + } +} + // New creates a new OrderedMap. // options can either be one or several InitOption[K, V], or a single integer, // which is then interpreted as a capacity hint, à la make(map[K]V, capacity). @@ -63,6 +72,11 @@ func New[K comparable, V any](options ...any) *OrderedMap[K, V] { invalidOption() } config.capacity = option + case bool: + if len(options) != 1 { + invalidOption() + } + config.disableHTMLEscape = option case InitOption[K, V]: option(&config) @@ -72,7 +86,7 @@ func New[K comparable, V any](options ...any) *OrderedMap[K, V] { } } - orderedMap.initialize(config.capacity) + orderedMap.initialize(config.capacity, config.disableHTMLEscape) orderedMap.AddPairs(config.initialData...) return orderedMap @@ -82,9 +96,10 @@ const invalidOptionMessage = `when using orderedmap.New[K,V]() with options, eit func invalidOption() { panic(invalidOptionMessage) } -func (om *OrderedMap[K, V]) initialize(capacity int) { +func (om *OrderedMap[K, V]) initialize(capacity int, disableHTMLEscape bool) { om.pairs = make(map[K]*Pair[K, V], capacity) om.list = list.New[*Pair[K, V]]() + om.disableHTMLEscape = disableHTMLEscape } // Get looks for the given key, and returns the value associated with it, diff --git a/yaml.go b/yaml.go index 6022471..75c2efb 100644 --- a/yaml.go +++ b/yaml.go @@ -50,7 +50,7 @@ func (om *OrderedMap[K, V]) UnmarshalYAML(value *yaml.Node) error { } if om.list == nil { - om.initialize(0) + om.initialize(0, om.disableHTMLEscape) } for index := 0; index < len(value.Content); index += 2 {