Skip to content

Commit

Permalink
fix #27 retain line break (#42)
Browse files Browse the repository at this point in the history
* feat: implement retain line breaks after formatting

* test: add tests for retainnig line breaks
  • Loading branch information
longkai committed Sep 10, 2022
1 parent d9e8674 commit e05c99f
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 13 deletions.
33 changes: 20 additions & 13 deletions formatters/basic/formatter.go
Expand Up @@ -43,7 +43,25 @@ func (f *BasicFormatter) Format(yamlContent []byte) ([]byte, error) {
reader = bytes.NewReader(yamlContent)
}

decoder := yaml.NewDecoder(reader)
encodedContent, err := retainLineBreaks(reader, f.format)
if err != nil {
return nil, err
}

if f.Config.IncludeDocumentStart {
encodedContent = withDocumentStart(encodedContent)
}
if f.Config.EmojiSupport {
encodedContent = hotfix.ParseUnicodePoints(encodedContent)
}
if f.Config.LineEnding == yamlfmt.LineBreakStyleCRLF {
encodedContent = hotfix.WriteCRLFBytes(encodedContent)
}
return encodedContent, nil
}

func (f *BasicFormatter) format(in io.Reader) (io.Reader, error) {
decoder := yaml.NewDecoder(in)
documents := []yaml.Node{}
for {
var docNode yaml.Node
Expand All @@ -66,18 +84,7 @@ func (f *BasicFormatter) Format(yamlContent []byte) ([]byte, error) {
return nil, err
}
}

encodedContent := b.Bytes()
if f.Config.IncludeDocumentStart {
encodedContent = withDocumentStart(b.Bytes())
}
if f.Config.EmojiSupport {
encodedContent = hotfix.ParseUnicodePoints(encodedContent)
}
if f.Config.LineEnding == yamlfmt.LineBreakStyleCRLF {
encodedContent = hotfix.WriteCRLFBytes(encodedContent)
}
return encodedContent, nil
return &b, nil
}

func withDocumentStart(document []byte) []byte {
Expand Down
66 changes: 66 additions & 0 deletions formatters/basic/formatter_test.go
Expand Up @@ -113,3 +113,69 @@ func TestEmojiSupport(t *testing.T) {
t.Fatalf("expected string to contain 😊, got: %s", resultStr)
}
}

func TestRetainLineBreaks(t *testing.T) {
testCases := []struct {
desc string
input string
expect string
}{
{
desc: "basic",
input: `a: 1
b: 2`,
expect: `a: 1
b: 2
`,
},
{
desc: "multi-doc",
input: `a: 1
# tail comment
---
b: 2`,
expect: `a: 1
# tail comment
---
b: 2
`,
},
{
desc: "literal string",
input: `a: 1
shell: |
#!/usr/bin/env bash
# hello, world
# bye
echo "hello, world"
`,
expect: `a: 1
shell: |
#!/usr/bin/env bash
# hello, world
# bye
echo "hello, world"
`,
},
}
f := &basic.BasicFormatter{Config: basic.DefaultConfig()}
for _, c := range testCases {
t.Run(c.desc, func(t *testing.T) {
got, err := f.Format([]byte(c.input))
if err != nil {
t.Fatalf("expected formatting to pass, returned error: %v", err)
}
if string(got) != c.expect {
t.Fatalf("didn't retain line breaks result: %v, expect %s", string(got), c.expect)
}
})
}
}
82 changes: 82 additions & 0 deletions formatters/basic/line_break.go
@@ -0,0 +1,82 @@
package basic

import (
"bufio"
"bytes"
"io"
"strings"
)

const lineBreakPlaceholder = "#magic___^_^___line"

// retainLineBreaks keeps the line breaks.
//
// The basic idea is to insert/remove placeholder comments in the yaml document before and after the format process.
func retainLineBreaks(in io.Reader, formatter func(io.Reader) (io.Reader, error)) ([]byte, error) {
var buf bytes.Buffer
scanner := bufio.NewScanner(in)
var padding paddinger
for scanner.Scan() {
txt := scanner.Text()
padding.adjust(indentLen(txt))
if strings.TrimSpace(txt) == "" { // line break or empty space line.
buf.WriteString(padding.String()) // prepend some padding incase literal multiline strings.
buf.WriteString(lineBreakPlaceholder)
buf.WriteByte('\n')
continue
}
buf.WriteString(txt)
buf.WriteByte('\n')
}
if err := scanner.Err(); err != nil {
return nil, err
}

formatted, err := formatter(&buf)
if err != nil {
return nil, err
}

buf.Reset() // reuse

scanner = bufio.NewScanner(formatted)
for scanner.Scan() {
txt := scanner.Text()
if strings.TrimSpace(txt) == "" {
// The basic yaml lib inserts newline when there is a comment(either placeholder or by user)
// followed by optional line breaks and a `---` multi-documents.
// To fix it, the empty line could only be inserted by us.
continue
}
if strings.HasPrefix(strings.TrimLeft(txt, " "), lineBreakPlaceholder) {
buf.WriteByte('\n')
continue
}
buf.WriteString(txt)
buf.WriteByte('\n')
}

return buf.Bytes(), scanner.Err()
}

func indentLen(txt string) int {
var cnt int
for i := 0; i < len(txt); i++ {
if txt[i] != ' ' { // Yaml only allows space to indent.
break
}
cnt++
}
return cnt
}

type paddinger struct {
strings.Builder
}

func (p *paddinger) adjust(size int) {
// Grows if the given size is larger than us and always return the max padding.
for diff := size - p.Len(); diff > 0; diff-- {
p.WriteByte(' ')
}
}

0 comments on commit e05c99f

Please sign in to comment.