diff --git a/decode_test.go b/decode_test.go index 50d633f7..166f56c7 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.go b/printer/printer.go index f3e63330..e2fb6160 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) } diff --git a/printer/printer_test.go b/printer/printer_test.go index dc8d4bdb..b20a96ac 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 b36deea0..b21e4d3d 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 }