You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
352 lines
7.9 KiB
352 lines
7.9 KiB
package printer |
|
|
|
import ( |
|
"fmt" |
|
"math" |
|
"strings" |
|
|
|
"github.com/fatih/color" |
|
"github.com/goccy/go-yaml/ast" |
|
"github.com/goccy/go-yaml/token" |
|
) |
|
|
|
// Property additional property set for each the token |
|
type Property struct { |
|
Prefix string |
|
Suffix string |
|
} |
|
|
|
// PrintFunc returns property instance |
|
type PrintFunc func() *Property |
|
|
|
// Printer create text from token collection or ast |
|
type Printer struct { |
|
LineNumber bool |
|
LineNumberFormat func(num int) string |
|
MapKey PrintFunc |
|
Anchor PrintFunc |
|
Alias PrintFunc |
|
Bool PrintFunc |
|
String PrintFunc |
|
Number PrintFunc |
|
} |
|
|
|
func defaultLineNumberFormat(num int) string { |
|
return fmt.Sprintf("%2d | ", num) |
|
} |
|
|
|
func (p *Printer) property(tk *token.Token) *Property { |
|
prop := &Property{} |
|
switch tk.PreviousType() { |
|
case token.AnchorType: |
|
if p.Anchor != nil { |
|
return p.Anchor() |
|
} |
|
return prop |
|
case token.AliasType: |
|
if p.Alias != nil { |
|
return p.Alias() |
|
} |
|
return prop |
|
} |
|
switch tk.NextType() { |
|
case token.MappingValueType: |
|
if p.MapKey != nil { |
|
return p.MapKey() |
|
} |
|
return prop |
|
} |
|
switch tk.Type { |
|
case token.BoolType: |
|
if p.Bool != nil { |
|
return p.Bool() |
|
} |
|
return prop |
|
case token.AnchorType: |
|
if p.Anchor != nil { |
|
return p.Anchor() |
|
} |
|
return prop |
|
case token.AliasType: |
|
if p.Anchor != nil { |
|
return p.Alias() |
|
} |
|
return prop |
|
case token.StringType, token.SingleQuoteType, token.DoubleQuoteType: |
|
if p.String != nil { |
|
return p.String() |
|
} |
|
return prop |
|
case token.IntegerType, token.FloatType: |
|
if p.Number != nil { |
|
return p.Number() |
|
} |
|
return prop |
|
default: |
|
} |
|
return prop |
|
} |
|
|
|
// PrintTokens create text from token collection |
|
func (p *Printer) PrintTokens(tokens token.Tokens) string { |
|
if len(tokens) == 0 { |
|
return "" |
|
} |
|
if p.LineNumber { |
|
if p.LineNumberFormat == nil { |
|
p.LineNumberFormat = defaultLineNumberFormat |
|
} |
|
} |
|
texts := []string{} |
|
lineNumber := tokens[0].Position.Line |
|
for _, tk := range tokens { |
|
lines := strings.Split(tk.Origin, "\n") |
|
prop := p.property(tk) |
|
header := "" |
|
if p.LineNumber { |
|
header = p.LineNumberFormat(lineNumber) |
|
} |
|
if len(lines) == 1 { |
|
line := prop.Prefix + lines[0] + prop.Suffix |
|
if len(texts) == 0 { |
|
texts = append(texts, header+line) |
|
lineNumber++ |
|
} else { |
|
text := texts[len(texts)-1] |
|
texts[len(texts)-1] = text + line |
|
} |
|
} else { |
|
for idx, src := range lines { |
|
if p.LineNumber { |
|
header = p.LineNumberFormat(lineNumber) |
|
} |
|
line := prop.Prefix + src + prop.Suffix |
|
if idx == 0 { |
|
if len(texts) == 0 { |
|
texts = append(texts, header+line) |
|
lineNumber++ |
|
} else { |
|
text := texts[len(texts)-1] |
|
texts[len(texts)-1] = text + line |
|
} |
|
} else { |
|
texts = append(texts, fmt.Sprintf("%s%s", header, line)) |
|
lineNumber++ |
|
} |
|
} |
|
} |
|
} |
|
return strings.Join(texts, "\n") |
|
} |
|
|
|
// PrintNode create text from ast.Node |
|
func (p *Printer) PrintNode(node ast.Node) []byte { |
|
return []byte(fmt.Sprintf("%+v\n", node)) |
|
} |
|
|
|
const escape = "\x1b" |
|
|
|
func format(attr color.Attribute) string { |
|
return fmt.Sprintf("%s[%dm", escape, attr) |
|
} |
|
|
|
func (p *Printer) setDefaultColorSet() { |
|
p.Bool = func() *Property { |
|
return &Property{ |
|
Prefix: format(color.FgHiMagenta), |
|
Suffix: format(color.Reset), |
|
} |
|
} |
|
p.Number = func() *Property { |
|
return &Property{ |
|
Prefix: format(color.FgHiMagenta), |
|
Suffix: format(color.Reset), |
|
} |
|
} |
|
p.MapKey = func() *Property { |
|
return &Property{ |
|
Prefix: format(color.FgHiCyan), |
|
Suffix: format(color.Reset), |
|
} |
|
} |
|
p.Anchor = func() *Property { |
|
return &Property{ |
|
Prefix: format(color.FgHiYellow), |
|
Suffix: format(color.Reset), |
|
} |
|
} |
|
p.Alias = func() *Property { |
|
return &Property{ |
|
Prefix: format(color.FgHiYellow), |
|
Suffix: format(color.Reset), |
|
} |
|
} |
|
p.String = func() *Property { |
|
return &Property{ |
|
Prefix: format(color.FgHiGreen), |
|
Suffix: format(color.Reset), |
|
} |
|
} |
|
} |
|
|
|
func (p *Printer) PrintErrorMessage(msg string, isColored bool) string { |
|
if isColored { |
|
return fmt.Sprintf("%s%s%s", |
|
format(color.FgHiRed), |
|
msg, |
|
format(color.Reset), |
|
) |
|
} |
|
return msg |
|
} |
|
|
|
func (p *Printer) removeLeftSideNewLineChar(src string) string { |
|
return strings.TrimLeft(strings.TrimLeft(strings.TrimLeft(src, "\r"), "\n"), "\r\n") |
|
} |
|
|
|
func (p *Printer) removeRightSideNewLineChar(src string) string { |
|
return strings.TrimRight(strings.TrimRight(strings.TrimRight(src, "\r"), "\n"), "\r\n") |
|
} |
|
|
|
func (p *Printer) removeRightSideWhiteSpaceChar(src string) string { |
|
return p.removeRightSideNewLineChar(strings.TrimRight(src, " ")) |
|
} |
|
|
|
func (p *Printer) newLineCount(s string) int { |
|
src := []rune(s) |
|
size := len(src) |
|
cnt := 0 |
|
for i := 0; i < size; i++ { |
|
c := src[i] |
|
switch c { |
|
case '\r': |
|
if i+1 < size && src[i+1] == '\n' { |
|
i++ |
|
} |
|
cnt++ |
|
case '\n': |
|
cnt++ |
|
} |
|
} |
|
return cnt |
|
} |
|
|
|
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) printBeforeTokens(tk *token.Token, minLine, extLine int) token.Tokens { |
|
for { |
|
if tk.Prev == nil { |
|
break |
|
} |
|
if tk.Prev.Position.Line < minLine { |
|
break |
|
} |
|
tk = tk.Prev |
|
} |
|
minTk := tk.Clone() |
|
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 { |
|
clonedTk := tk.Clone() |
|
tokens.Add(clonedTk) |
|
tk = clonedTk.Next |
|
} |
|
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 { |
|
next := lastTk.Next.Clone() |
|
// add suffix to header of next token |
|
if suffix[0] == '\n' || suffix[0] == '\r' { |
|
suffix = suffix[1:] |
|
} |
|
next.Origin = suffix + next.Origin |
|
lastTk.Next = next |
|
} |
|
return tokens |
|
} |
|
|
|
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 |
|
} |
|
minTk := tk.Clone() |
|
minTk.Origin = p.removeLeftSideNewLineChar(minTk.Origin) |
|
tokens.Add(minTk) |
|
tk = minTk.Next |
|
for tk != nil && tk.Position.Line <= maxLine { |
|
clonedTk := tk.Clone() |
|
tokens.Add(clonedTk) |
|
tk = clonedTk.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() |
|
return fn(prefix(annotateLine, num)) |
|
} |
|
return prefix(annotateLine, num) |
|
} |
|
if isColored { |
|
p.setDefaultColorSet() |
|
} |
|
} |
|
|
|
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-- |
|
} |
|
|
|
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) |
|
}
|
|
|