Repository: goccy/go-yaml
Pull request #117 (2020-06-07 11:29:44)

Fix output format for PrintErrorToken
We will get more beautiful result in PrintErrorToken .


Patch:
diff --git a/printer/printer.go b/printer/printer.go
index f3e6333..e2fb616 100644
--- a/printer/printer.go
+++ b/printer/printer.go
@@ -231,80 +231,134 @@ func (p *Printer) newLineCount(s string) int {
 	return cnt
 }
 
-func (p *Printer) isNewLineChar(c byte) bool {
-	if c == '\n' {
-		return true
-	}
-	if c == '\r' {
-		return true
+func (p *Printer) isNewLineLastChar(s string) bool {
+	for i := len(s) - 1; i > 0; i-- {
+		c := s[i]
+		switch c {
+		case ' ':
+			continue
+		case '\n', '\r':
+			return true
+		}
+		break
 	}
 	return false
 }
 
-func (p *Printer) PrintErrorToken(tk *token.Token, isColored bool) string {
-	errToken := tk
-	pos := tk.Position
-	curLine := pos.Line
-	curExtLine := curLine + p.newLineCount(p.removeLeftSideNewLineChar(tk.Origin))
-	if p.isNewLineChar(tk.Origin[len(tk.Origin)-1]) {
-		// if last character is new line character, ignore it.
-		curExtLine--
-	}
-	minLine := int(math.Max(float64(curLine-3), 1))
-	maxLine := curExtLine + 3
+func (p *Printer) printBeforeTokens(tk *token.Token, minLine, extLine int) token.Tokens {
 	for {
-		if tk.Position.Line < minLine {
+		if tk.Prev == nil {
 			break
 		}
-		if tk.Prev == nil {
+		if tk.Prev.Position.Line < minLine {
 			break
 		}
 		tk = tk.Prev
 	}
-	tokens := token.Tokens{}
-	lastTk := tk
-	for tk.Position.Line <= curExtLine {
+	minTk := tk
+	if minTk.Prev != nil {
+		// add white spaces to minTk by prev token
+		prev := minTk.Prev
+		whiteSpaceLen := len(prev.Origin) - len(strings.TrimRight(prev.Origin, " "))
+		minTk.Origin = strings.Repeat(" ", whiteSpaceLen) + minTk.Origin
+	}
+	minTk.Origin = p.removeLeftSideNewLineChar(minTk.Origin)
+	tokens := token.Tokens{minTk}
+	tk = minTk.Next
+	for tk != nil && tk.Position.Line <= extLine {
 		tokens.Add(tk)
-		lastTk = tk
 		tk = tk.Next
-		if tk == nil {
-			break
+	}
+	lastTk := tokens[len(tokens)-1]
+	trimmedOrigin := p.removeRightSideWhiteSpaceChar(lastTk.Origin)
+	suffix := lastTk.Origin[len(trimmedOrigin):]
+	lastTk.Origin = trimmedOrigin
+
+	if lastTk.Next != nil && len(suffix) > 1 {
+		// add suffix to header of next token
+		if suffix[0] == '\n' || suffix[0] == '\r' {
+			suffix = suffix[1:]
 		}
+		lastTk.Next.Origin = suffix + lastTk.Next.Origin
+	}
+	return tokens
+}
+
+func (p *Printer) addNewLineCharIfDocumentHeader(tk *token.Token) {
+	if tk.Prev == nil {
+		return
+	}
+	if tk.Type != token.DocumentHeaderType {
+		return
+	}
+	prev := tk.Prev
+	lineDiff := tk.Position.Line - prev.Position.Line
+	if p.isNewLineLastChar(prev.Origin) {
+		lineDiff--
+	}
+	tk.Origin = strings.Repeat("\n", lineDiff) + tk.Origin
+}
+
+func (p *Printer) printAfterTokens(tk *token.Token, maxLine int) token.Tokens {
+	tokens := token.Tokens{}
+	if tk == nil {
+		return tokens
+	}
+	if tk.Position.Line > maxLine {
+		return tokens
 	}
-	org := lastTk.Origin
-	trimmed := p.removeRightSideWhiteSpaceChar(org)
-	lastTk.Origin = trimmed
-	if tk != nil {
-		tk.Origin = p.removeLeftSideNewLineChar(tk.Origin)
+	minTk := tk
+	minTk.Origin = p.removeLeftSideNewLineChar(minTk.Origin)
+	tokens.Add(minTk)
+	tk = minTk.Next
+	for tk != nil && tk.Position.Line <= maxLine {
+		p.addNewLineCharIfDocumentHeader(tk)
+		tokens.Add(tk)
+		tk = tk.Next
+	}
+	return tokens
+}
+
+func (p *Printer) setupErrorTokenFormat(annotateLine int, isColored bool) {
+	prefix := func(annotateLine, num int) string {
+		if annotateLine == num {
+			return fmt.Sprintf("> %2d | ", num)
+		}
+		return fmt.Sprintf("  %2d | ", num)
 	}
 	p.LineNumber = true
 	p.LineNumberFormat = func(num int) string {
 		if isColored {
 			fn := color.New(color.Bold, color.FgHiWhite).SprintFunc()
-			if curLine == num {
-				return fn(fmt.Sprintf("> %2d | ", num))
-			}
-			return fn(fmt.Sprintf("  %2d | ", num))
-		}
-		if curLine == num {
-			return fmt.Sprintf("> %2d | ", num)
+			return fn(prefix(annotateLine, num))
 		}
-		return fmt.Sprintf("  %2d | ", num)
+		return prefix(annotateLine, num)
 	}
 	if isColored {
 		p.setDefaultColorSet()
 	}
-	beforeSource := p.PrintTokens(tokens)
-	prefixSpaceNum := len(fmt.Sprintf("  %2d | ", 1))
-	annotateLine := strings.Repeat(" ", prefixSpaceNum+errToken.Position.Column-2) + "^"
-	tokens = token.Tokens{}
-	for tk != nil {
-		if tk.Position.Line > maxLine {
-			break
-		}
-		tokens.Add(tk)
-		tk = tk.Next
+}
+
+func (p *Printer) PrintErrorToken(tk *token.Token, isColored bool) string {
+	errToken := tk
+	curLine := tk.Position.Line
+	curExtLine := curLine + p.newLineCount(p.removeLeftSideNewLineChar(tk.Origin))
+	if p.isNewLineLastChar(tk.Origin) {
+		// if last character ( exclude white space ) is new line character, ignore it.
+		curExtLine--
 	}
-	afterSource := p.PrintTokens(tokens)
+
+	minLine := int(math.Max(float64(curLine-3), 1))
+	maxLine := curExtLine + 3
+	p.setupErrorTokenFormat(curLine, isColored)
+
+	beforeTokens := p.printBeforeTokens(tk, minLine, curExtLine)
+	lastTk := beforeTokens[len(beforeTokens)-1]
+	afterTokens := p.printAfterTokens(lastTk.Next, maxLine)
+
+	beforeSource := p.PrintTokens(beforeTokens)
+	prefixSpaceNum := len(fmt.Sprintf("  %2d | ", curLine))
+	annotateLine := strings.Repeat(" ", prefixSpaceNum+errToken.Position.Column-1) + "^"
+	afterSource := p.PrintTokens(afterTokens)
 	return fmt.Sprintf("%s\n%s\n%s", beforeSource, annotateLine, afterSource)
 }


Test patch:
diff --git a/decode_test.go b/decode_test.go
index 50d633f..166f56c 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -1487,10 +1487,9 @@ a: c
 `
 	expected := `
 [3:1] duplicate key "a"
-   2 | 
->  3 | a: b
-   4 | a: c
-      ^
+   2 | a: b
+>  3 | a: c
+       ^
 `
 	var v map[string]string
 	err := yaml.NewDecoder(strings.NewReader(yml), yaml.DisallowDuplicateKey()).Decode(&v)
@@ -1587,7 +1586,7 @@ complecated: string
 	//        1 | ---
 	//        2 | simple: string
 	//     >  3 | complecated: string
-	//           ^
+	//            ^
 }
 
 type unmarshalableStringValue string
diff --git a/printer/printer_test.go b/printer/printer_test.go
index dc8d4bd..b20a96a 100644
--- a/printer/printer_test.go
+++ b/printer/printer_test.go
@@ -32,7 +32,7 @@ alias: *x
 		expect := `
    1 | ---
 >  2 | text: aaaa
-            ^
+             ^
    3 | text2: aaaa
    4 |  bbbb
    5 |  cccc
@@ -55,7 +55,7 @@ alias: *x
    5 |  cccc
    6 |  dddd
    7 |  eeee
-      ^
+       ^
 `
 		if actual != expect {
 			t.Fatalf("unexpected output: expect:[%s]\n actual:[%s]", expect, actual)
@@ -73,7 +73,7 @@ alias: *x
    5 |  cccc
    6 |  dddd
    7 |  eeee
-             ^
+              ^
    8 | text3: ffff
    9 |  gggg
   10 |  hhhh
@@ -84,21 +84,47 @@ alias: *x
 			t.Fatalf("unexpected output: expect:[%s]\n actual:[%s]", expect, actual)
 		}
 	})
+	t.Run("print error token with document header", func(t *testing.T) {
+		tokens := lexer.Tokenize(`---
+a:
+ b:
+  c:
+   d: e
+   f: g
+   h: i
+
+---
+`)
+		expect := `
+   3 |  b:
+   4 |   c:
+   5 |    d: e
+>  6 |    f: g
+             ^
+   7 |    h: i
+   8 | 
+   9 | ---`
+		var p printer.Printer
+		actual := "\n" + p.PrintErrorToken(tokens[12], false)
+		if actual != expect {
+			t.Fatalf("unexpected output: expect:[%s]\n actual:[%s]", expect, actual)
+		}
+	})
 	t.Run("output with color", func(t *testing.T) {
 		t.Run("token6", func(t *testing.T) {
 			tokens := lexer.Tokenize(yml)
 			var p printer.Printer
-			t.Logf("%s", p.PrintErrorToken(tokens[6], true))
+			t.Logf("\n%s", p.PrintErrorToken(tokens[6], true))
 		})
 		t.Run("token9", func(t *testing.T) {
 			tokens := lexer.Tokenize(yml)
 			var p printer.Printer
-			t.Logf("%s", p.PrintErrorToken(tokens[9], true))
+			t.Logf("\n%s", p.PrintErrorToken(tokens[9], true))
 		})
 		t.Run("token12", func(t *testing.T) {
 			tokens := lexer.Tokenize(yml)
 			var p printer.Printer
-			t.Logf("%s", p.PrintErrorToken(tokens[12], true))
+			t.Logf("\n%s", p.PrintErrorToken(tokens[12], true))
 		})
 	})
 	t.Run("print error message", func(t *testing.T) {
diff --git a/validate_test.go b/validate_test.go
index b36deea..b21e4d3 100644
--- a/validate_test.go
+++ b/validate_test.go
@@ -35,12 +35,11 @@ func ExampleStructValidator() {
 	fmt.Printf("%v", err)
 	// OUTPUT:
 	// [5:8] Key: 'Person.Age' Error:Field validation for 'Age' failed on the 'gte' tag
-	//        1 | ---
 	//        2 | - name: john
 	//        3 |   age: 20
 	//        4 | - name: tom
 	//     >  5 |   age: -1
-	//                  ^
+	//                   ^
 	//        6 | - name: ken
 	//        7 |   age: 10
 }