From c559864862493fbc949c864f72c78b8d06ddf5e5 Mon Sep 17 00:00:00 2001 From: Shuhei Kitagawa Date: Wed, 22 Jan 2025 09:12:12 +0100 Subject: [PATCH] Fix indentation for raw string newlines https://github.com/goccy/go-yaml/issues/292 --- ast/ast.go | 10 ++++--- encode.go | 47 +++++++++++++++-------------- encode_test.go | 80 ++++++++++++++++++++++++++++++++------------------ option.go | 2 +- 4 files changed, 83 insertions(+), 56 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index 357ca816..d54fb206 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -816,11 +816,12 @@ func (n *StringNode) String() string { // It works mostly, but inconsistencies occur if line break characters are mixed. header := token.LiteralBlockHeader(n.Value) space := strings.Repeat(" ", n.Token.Position.Column-1) + indent := strings.Repeat(" ", n.Token.Position.IndentNum) values := []string{} for _, v := range strings.Split(n.Value, lbc) { - values = append(values, fmt.Sprintf("%s %s", space, v)) + values = append(values, fmt.Sprintf("%s%s%s", space, indent, v)) } - block := strings.TrimSuffix(strings.TrimSuffix(strings.Join(values, lbc), fmt.Sprintf("%s %s", lbc, space)), fmt.Sprintf(" %s", space)) + block := strings.TrimSuffix(strings.TrimSuffix(strings.Join(values, lbc), fmt.Sprintf("%s%s%s", lbc, indent, space)), fmt.Sprintf("%s%s", indent, space)) return fmt.Sprintf("%s%s%s", header, lbc, block) } else if len(n.Value) > 0 && (n.Value[0] == '{' || n.Value[0] == '[') { return fmt.Sprintf(`'%s'`, n.Value) @@ -847,11 +848,12 @@ func (n *StringNode) stringWithoutComment() string { // It works mostly, but inconsistencies occur if line break characters are mixed. header := token.LiteralBlockHeader(n.Value) space := strings.Repeat(" ", n.Token.Position.Column-1) + indent := strings.Repeat(" ", n.Token.Position.IndentNum) values := []string{} for _, v := range strings.Split(n.Value, lbc) { - values = append(values, fmt.Sprintf("%s %s", space, v)) + values = append(values, fmt.Sprintf("%s%s%s", space, indent, v)) } - block := strings.TrimSuffix(strings.TrimSuffix(strings.Join(values, lbc), fmt.Sprintf("%s %s", lbc, space)), fmt.Sprintf(" %s", space)) + block := strings.TrimSuffix(strings.TrimSuffix(strings.Join(values, lbc), fmt.Sprintf("%s%s%s", lbc, indent, space)), fmt.Sprintf(" %s", space)) return fmt.Sprintf("%s%s%s", header, lbc, block) } else if len(n.Value) > 0 && (n.Value[0] == '{' || n.Value[0] == '[') { return fmt.Sprintf(`'%s'`, n.Value) diff --git a/encode.go b/encode.go index 027b1be2..bb36f0fc 100644 --- a/encode.go +++ b/encode.go @@ -28,8 +28,6 @@ const ( type Encoder struct { writer io.Writer opts []EncodeOption - indent int - indentSequence bool singleQuote bool isFlowStyle bool isJSONStyle bool @@ -41,11 +39,12 @@ type Encoder struct { commentMap map[*Path][]*Comment written bool - line int - column int - offset int - indentNum int - indentLevel int + line int + column int + offset int + indentNum int + indentLevel int + indentSequence bool } // NewEncoder returns a new encoder that writes to w. @@ -54,12 +53,12 @@ func NewEncoder(w io.Writer, opts ...EncodeOption) *Encoder { return &Encoder{ writer: w, opts: opts, - indent: DefaultIndentSpaces, anchorPtrToNameMap: map[uintptr]string{}, customMarshalerMap: map[reflect.Type]func(interface{}) ([]byte, error){}, line: 1, column: 1, offset: 0, + indentNum: DefaultIndentSpaces, } } @@ -573,8 +572,13 @@ func (e *Encoder) encodeBool(v bool) *ast.BoolNode { func (e *Encoder) encodeSlice(ctx context.Context, value reflect.Value) (*ast.SequenceNode, error) { if e.indentSequence { - e.column += e.indent + e.column += e.indentNum } + defer func() { + if e.indentSequence { + e.column -= e.indentNum + } + }() column := e.column sequence := ast.Sequence(token.New("-", "-", e.pos(column)), e.isFlowStyle) for i := 0; i < value.Len(); i++ { @@ -584,16 +588,18 @@ func (e *Encoder) encodeSlice(ctx context.Context, value reflect.Value) (*ast.Se } sequence.Values = append(sequence.Values, node) } - if e.indentSequence { - e.column -= e.indent - } return sequence, nil } func (e *Encoder) encodeArray(ctx context.Context, value reflect.Value) (*ast.SequenceNode, error) { if e.indentSequence { - e.column += e.indent + e.column += e.indentNum } + defer func() { + if e.indentSequence { + e.column -= e.indentNum + } + }() column := e.column sequence := ast.Sequence(token.New("-", "-", e.pos(column)), e.isFlowStyle) for i := 0; i < value.Len(); i++ { @@ -603,9 +609,6 @@ func (e *Encoder) encodeArray(ctx context.Context, value reflect.Value) (*ast.Se } sequence.Values = append(sequence.Values, node) } - if e.indentSequence { - e.column -= e.indent - } return sequence, nil } @@ -617,7 +620,7 @@ func (e *Encoder) encodeMapItem(ctx context.Context, item MapItem, column int) ( return nil, err } if e.isMapNode(value) { - value.AddColumn(e.indent) + value.AddColumn(e.indentNum) } return ast.MappingValue( token.New("", "", e.pos(column)), @@ -660,7 +663,7 @@ func (e *Encoder) encodeMap(ctx context.Context, value reflect.Value, column int return nil } if e.isMapNode(value) { - value.AddColumn(e.indent) + value.AddColumn(e.indentNum) } node.Values = append(node.Values, ast.MappingValue( nil, @@ -783,7 +786,7 @@ func (e *Encoder) encodeStruct(ctx context.Context, value reflect.Value, column return nil, err } if e.isMapNode(value) { - value.AddColumn(e.indent) + value.AddColumn(e.indentNum) } var key ast.MapKeyNode = e.encodeString(structField.RenderName, column) switch { @@ -833,8 +836,8 @@ func (e *Encoder) encodeStruct(ctx context.Context, value reflect.Value, column // if declared same key name, skip encoding this field continue } - key.AddColumn(-e.indent) - value.AddColumn(-e.indent) + key.AddColumn(-e.indentNum) + value.AddColumn(-e.indentNum) node.Values = append(node.Values, ast.MappingValue(nil, key, value)) } continue @@ -848,7 +851,7 @@ func (e *Encoder) encodeStruct(ctx context.Context, value reflect.Value, column node.Values = append(node.Values, ast.MappingValue(nil, key, value)) } if hasInlineAnchorField { - node.AddColumn(e.indent) + node.AddColumn(e.indentNum) anchorName := "anchor" anchorNode := ast.Anchor(token.New("&", "&", e.pos(column))) anchorNode.Name = ast.String(token.New(anchorName, anchorName, e.pos(column))) diff --git a/encode_test.go b/encode_test.go index 1d9b9bd4..376964a2 100644 --- a/encode_test.go +++ b/encode_test.go @@ -1540,49 +1540,71 @@ func TestIssue356(t *testing.T) { } func TestMarshalIndentWithMultipleText(t *testing.T) { - t.Run("depth1", func(t *testing.T) { - b, err := yaml.MarshalWithOptions(map[string]interface{}{ - "key": []string{`line1 + tests := []struct { + name string + input map[string]interface{} + indent yaml.EncodeOption + want string + }{ + { + name: "depth1", + input: map[string]interface{}{ + "key": []string{`line1 line2 line3`}, - }, yaml.Indent(2)) - if err != nil { - t.Fatal(err) - } - got := string(b) - expected := `key: + }, + indent: yaml.Indent(2), + want: `key: - |- line1 line2 line3 -` - if expected != got { - t.Fatalf("failed to encode.\nexpected:\n%s\nbut got:\n%s\n", expected, got) - } - }) - t.Run("depth2", func(t *testing.T) { - b, err := yaml.MarshalWithOptions(map[string]interface{}{ - "key": map[string]interface{}{ - "key2": []string{`line1 +`, + }, + { + name: "depth2", + input: map[string]interface{}{ + "key": map[string]interface{}{ + "key2": []string{`line1 line2 line3`}, + }, }, - }, yaml.Indent(2)) - if err != nil { - t.Fatal(err) - } - got := string(b) - expected := `key: + indent: yaml.Indent(2), + want: `key: key2: - |- line1 line2 line3 -` - if expected != got { - t.Fatalf("failed to encode.\nexpected:\n%s\nbut got:\n%s\n", expected, got) - } - }) +`, + }, + { + name: "raw string new lines", + input: map[string]interface{}{ + "key": "line1\nline2\nline3", + }, + indent: yaml.Indent(4), + want: `key: |- + line1 + line2 + line3 +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := yaml.MarshalWithOptions(tt.input, tt.indent) + if err != nil { + t.Fatalf("failed to marshal yaml: %v", err) + } + got := string(b) + if tt.want != got { + t.Fatalf("failed to encode.\nexpected:\n%s\nbut got:\n%s\n", tt.want, got) + } + }) + } } type bytesMarshaler struct{} diff --git a/option.go b/option.go index 33100e9f..bd685e2f 100644 --- a/option.go +++ b/option.go @@ -114,7 +114,7 @@ type EncodeOption func(e *Encoder) error // Indent change indent number func Indent(spaces int) EncodeOption { return func(e *Encoder) error { - e.indent = spaces + e.indentNum = spaces return nil } }